异步编程系列教程:

  1. (翻译)异步编程之Promise(1)——初见魅力
  2. 异步编程之Promise(2):探究原理
  3. 异步编程之Promise(3):拓展进阶
  4. 异步编程之Generator(1)——领略魅力
  5. 异步编程之Generator(2)——剖析特性
  6. 异步编程之co——源码分析

拓展功能

在前面的文章中,通过了解promise能做什么,实践动手从原理上了解promise/deferred模式的用法,相信大家应该更期待这次的功能拓展。我们不仅需要让单异步操作promise化,我们还需要从实际出发,拓展更多有用的功能。直接看一下我们这一次需要做的两个功能:

  1. 多异步并行控制
  2. 多异步串行队列

这两个功能用我们之前自己写的简陋promise库,是无法做到的。我们不能在指定多个promise异步完成后,再触发回调。也不能让多个promise异步像排队一样,一个一个的进行,甚至下一个promise的参数是依赖上一个promise的。这就是我们接下来需要解决的问题:

多异步并行控制

在冻手之前,我们先想一想大致的思路吧。首先我们肯定是并发了多个异步,我们需要做的仅仅就是,监控所有并发的异步,并让最后一个异步触发resolve回调函数。当然错误处理的话,就是当有一个异步错误,直接就reject掉宣布异步失败结束。一般监视并发,我们都会有一个哨兵变量,每完成一个异步,就对哨兵进行维护并检测异步是否结束。

那我们的API应该怎么设置呢?朴灵老师的书上是这样的:deferred.all([promise1, promise2]).then()。从这里我们可以看出,就是由各个小promise组成了一个大的promise,并在大promise中进行接下来的操作。一起看一下代码吧:

Deferred.prototype.all = function(promises){
var result = []; // 存储各个promise的执行结果
var count = promises.length; // 哨兵变量
var _this = this;
promises.forEach(function(promise, index){
promise.then(function(res){
result[index] = res;
count--;
// 当执行最后一个promise后, 调用大promise的resolve,并把result传进去
while(count === 0){
_this.resolve(result);
}
}, function(err){
// 有一个promise出错,立即return并执行大promise的reject
return _this.reject(err);
});
});
return this.promise;
};

我个人认为最不好懂的应该是_this到底指的是什么?看过上一篇的朋友,应该知道deferred是延迟对象来的,作用就是触发即将在then()中绑定的resolve()reject()。那这里的_this必然是指大的promise,我们看一下如何使用的:

// 已经定义好Promise化的readFile(),不懂的同学可以翻阅上一篇文章。
// 这段代码是输出两个文件里,字符串length最大的值。
var r1 = readFile("hello.txt", 'utf-8');
var r2 = readFile("hello2.txt", 'utf-8'); var deferred = new Deferred(); // 初始化一个延时对象。
deferred.all([r1, r2]).then(function(res){
console.log(res);
res = res.map(function(item){return item.length});
console.log(Math.max.apply(null, res));
});

That's easy, right?! 我们这里仅仅是实现原理,是不成熟的,若实际使用中,更推荐Q.js。现在我们将需要并行的promise放到一个数组里,不出错就会得到每一次并行的结果,并存储在result中,最后返回得到并进行相应处理。当然我们也可以很清楚感受到它的局限,并行的promise是相互独立无依赖的。当多个异步开始有依赖了,我们该怎么做呢?这就是我们接下来要讨论的。

多异步串行队列

一般来说,多异步串行执行,通过最简单的嵌套回调即可解决。但我们可以想象,我们最终的理想形态应该是链式结构的。res依赖以上的步骤,我们通过链式结构可以更清晰易懂,有助于我们进行流程控制。

--------嵌套回调---------
api1(function(v1){
api2(function(v1, v2){
api3(function(v2, v3){
api4(function(v3, res){
callback(res);
})
})
})
});
--------链式调用---------
promise()
.then(api1)
.then(api2)
.then(api3)
.then(function(res){
// 用res来做一些事情
})

还是从想开始,我们需要做到promise支持链式执行,第一感觉的数据结构就是队列,就是那个FIFO先进先出的队列。我们将所有的回调都压入队列中,完成一个就取一个出来执行。但是更关键的问题在于,前面一个promise的值,如何传到下一个promise中。朴灵大大在这里给出的解决方案是:Promise执行回调时,一旦检测到返回的是新的Promise对象,会将当前Deferred延迟对象中的promise引用换成新的Promise对象。而那个回调队列,也同样转移到了新Promise上。

不知道大家有没有听懂大概个意思,如果还是不太清楚,我们可以思考一下,再对比一下实现的代码,就应该能看懂了。这次我们需要对以往的代码,做一个较大的改变,我们不再使用events.EventEmitter来进行事件触发了。为了能链式的调用回调,我们会将事件触发放在数组队列里,并按顺序进行触发。因为代码进行了较大的改变,我们逐个逐个看代码。

var Promise = function(){
this.isPromise = true; // 用于确定是promise对象
this.queue = []; // 回调事件的队列
};
Promise.prototype.then = function(resolve, reject){
var handler = {};
if(typeof resolve === 'function'){
handler.resolve = resolve;
}
if(typeof reject === 'function'){
handler.reject = reject;
}
this.queue.push(handler); // 将回调事件推入到数组队列中
return this;
};

这一段代码,我们最重要的是定义了一个queue属性。它是用来存放在then(resolve, reject)中的resolvereject方法的。最后我们会将一次promise的回调函数,推入到queue属性里,以供deffered延迟对象使用。

var Deferred = function(){
this.promise = new Promise();
};
Deferred.prototype.resolve = function(data){
var handler; //用于存放当前的回调
// 若队列存在回调
while(handler = this.promise.queue.shift()){
if(handler && handler.resolve){
var ret = handler.resolve(data);
if(ret && ret.isPromise){
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
}
}
};
Deferred.prototype.reject = function(err){
var handler; //用于存放当前的回调
// 若队列存在回调
while(handler = this.promise.queue.shift()){
if(handler && handler.reject){
var ret = handler.reject(err);
if(ret && ret.isPromise){
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
}
}
};
Deferred.prototype.makeNodeResolver = function(){
var _this = this;
return function(err, res){
if(err) return _this.reject(err);
_this.resolve(res);
}
};

这里,和以往一样,每一个deferred对象都会有一个promise对象。并且重新定义了resolvereject的实现,不再和以往一样,简单的通过触发事件实现。我们仔细分析一下,到底deffered对象的方法做了些什么。我们就取其中一个resolve来看,首先我们将队列promise的回调队列queue最前端的handler推出来,若存在就执行回调。若回调执行的结果是一个新的promise(我们通过isPromise属性判断),我们就会进行一个替换。这里是实现的关键,我们将原来那个promise的queue属性存到新的新的promise上,然后将deferred对象当前的promise变成新的promise,最后返回出来。通过这一系列的操作,我们就可以将回调队列进行传递,并实现链式调用。

--------hello.txt---------
data.json --------data.json---------
{"message": "Hello World!"} --------代码应用---------
var fs = require('fs'); var readFile = function(file){
var deferred = new Deferred();
fs.readFile(file, 'utf-8', deferred.makeNodeResolver());
return deferred.promise;
};
var readJSON = function(file){
var deferred = new Deferred();
fs.readFile(file, 'utf-8', function(err, file){
if(err) return deferred.reject(err);
deferred.resolve(JSON.parse(file));
});
return deferred.promise;
}; readFile('hello.txt').then(function(file){
return readJSON(file);
}).then(function(data){
console.log(data.message);
}); // 或者利用更简洁的特性
readFile('hello.txt').then(readJSON).then(function(data){
console.log(data.message); // hello world!
});

最后这段代码是我们多异步并行队列的实际应用。我们定义了两个promise化的异步方法,一个是readFile,一个readJSON。我们的readJSON函数是依赖readFile的结果的,最后我们一样实现了需求。我们这次也仅仅是研究原理实现的代码,是不成熟的。在实际应用中,还是需要借助成熟的框架Q.js等。

API promise化的封装

我们可以发现,为了使代码实现promise,我们需要为现有的异步api都进行一次封装。为了某些特殊情况,我们可以自己动手用promise/deferred模式,进行手动封装实现功能。然后很多现有的API,我们是可以从中抽象出相同的部分,借助函数柯里化,进行批量promise转化的。

var wrapPromise = function(api){
return function(){
var deferred = new Deferred();
var args = [].slice.call(arguments, 0);
args.push(deferred.makeNodeResolver());
api.apply(null, args);
return deferred.promise;
};
};
var fs = require('fs');
var readFile = wrapPromise(fs.readFile);

我们通过wrapPromise(api),将实现的细节隐藏在内部,变化的仅仅是需要promise化的api。其实内部实现的细节也是很简单可以看懂的,就是将promise化后的参数取出来,再多加一个node传统形式的回调,一同apply进api中。我们通过简单的wrapPromise直接得到一个promise化的异步api。

总结

到此,promise三部曲,总算是讲完了。在我总结写blog时,也是做了比较多的思考,有些地方也可能表意不清。我们知道其实promise,其实是另一种形式的回调,只是它的形式我们更喜欢,也更自然。我们唯一会烦恼的是,我们需要为不同场景的异步api进行Promise化。但是为了更好的控制,我认为也是值得尝试的。promise单独使用,并不能体现它强大的地方。因为接下来我们会讲promise和Generator配合,展现强大的异步编程能力。

异步编程之Promise(3):拓展进阶的更多相关文章

  1. 异步编程之Promise(2):探究原理

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  2. (翻译)异步编程之Promise(1):初见魅力

    原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...

  3. ECMA Script 6_异步编程之 Promise

    Promise 对象 异步编程 方案,已同步的方式表达异步的代码,解决回调地狱的问题 比传统的解决方案——回调函数和事件——更合理和更强大 是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步 ...

  4. 前端异步编程之Promise和async的用法

    传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案: ES6的新语法Promise ES2017引入的async函数 Generator函数(略) Promise的含义 ...

  5. JavaScript的异步编程之Promise

    Promise 一种更优的异步编程统一 方法,如果直接使用传统的回调函数去完成复杂操作就会形成回调深渊 // 回调深渊 $.get('/url1'() => { $.get('/url2'() ...

  6. 异步编程之Generator(1)——领略魅力

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  7. 异步编程之co——源码分析

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  8. 异步编程之Generator(2)——剖析特性

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  9. Javascript异步编程之setTimeout与setInterval详解分析(一)

    Javascript异步编程之setTimeout与setInterval 在谈到异步编程时,本人最主要会从以下三个方面来总结异步编程(注意:特别解释:是总结,本人也是菜鸟,所以总结不好的,请各位大牛 ...

随机推荐

  1. SQL注入实验,PHP连接数据库,Mysql查看binlog,PreparedStatement,mysqli, PDO

    看到有人说了判断能否sql注入的方法: 简单的在参数后边加一个单引号,就可以快速判断是否可以进行SQL注入,这个百试百灵,如果有漏洞的话,一般会报错. 下面内容参考了这两篇文章 http://blog ...

  2. Android无法访问本地服务器(localhost)的解决方案

    在Android开发中通过localhost或127.0.0.1访问本地服务器时,会报java.net.ConnectException: localhost/127.0.0.1:8083 -Conn ...

  3. POI刷新数据后的函数(公式)更新问题

    使用POI将Excel模板中的数据进行更新,这应该是很常见的操作 下面就贴上我的一小段代码 public class ModifyExcel { /** * @param fileName Excel ...

  4. bzoj4199

    看到这题我就伤心,当初想到了正解却因为各种sb原因没有写…… 好吧,其实我的正解是比较挫的…… 大家似乎都用了后缀数组,我用了后缀自动机(后缀树) 其实SAM是很好想得,用SAM建出后缀树后 我们考虑 ...

  5. VS2015新功能

    今天有幸参加了微软的 Visual Studio Dev Day,趁还没有忘记今天的学习内容. 先把这些内容记录下来,如果有其他人也参加此次交流活动,请补充完善. VS2015新功能 1,Roslyn ...

  6. Android 系统属性

    /************************************************************************ * Android 系统属性 * 说明: * 由于需 ...

  7. java生成简单Excel工作薄

    前言: 代码都是建立在实际需求上的,上周做完一个调外部电影券接口的项目,这周产品又要excel表格,大致内容为:券所属影院.图片URL.等信息制作为excel表格,把每次同步过来的数据给他分析. jx ...

  8. JVM——类加载机制

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在Java语言中,类型的加载.连接和初始化过 ...

  9. 正确理解 AsyncTask,Looper,Handler三者之间的关系(基于android 4.0)

    Looper 和Handler 是理解好AsyncTask的一个基础,我们可以先从这里开始,先给出一个主线程和子线程互相通信的例子. package com.example.loopertest; i ...

  10. 解决 RaspberryPi 树莓派 NTP服务异常 无法自动同步时间

    sudo nano /etc/ntp.conf 然后找到 # pool.ntp.org maps to about 1000 low-stratum NTP servers. Your server ...