异步编程系列教程:

  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. Gliffy Diagrams 好用的流程图工具

    很好用!加上百度脑图!good!

  2. Qt Assistant介绍

    简介 Qt Assistant也就是我们常说的Qt助手,是一款用于呈现在线文档的工具. 简介 一分钟学会使用 Qt参考文档 Qt Assistant详解 命令行选项 工具窗口 文档窗口 工具栏 菜单 ...

  3. HDU 1158 Employment Planning【DP】

    题意:给出n个月,雇佣一个人所需的钱hire,一个人工作一个月所需要的钱salary,解雇一个人所需要的钱fire,再给出这n个月每月1至少有num[i]个人完成工作,问完成整个工作所花费的最少的钱是 ...

  4. Ref相关的名词解释

    NV (NOT-VOLATILE),即非易失性,断电不会丢失的存储信息,包括生产信息.客户信息.产品信息等等. 它们都保存在不同(FLASH)分区,并根据不同的分区提供不同的接口.数据结构和管理机制. ...

  5. Android AIDL自动生成Java文件测试

    /******************************************************************************** * Android AIDL自动生成 ...

  6. 信息熵 Information Theory

    信息论(Information Theory)是概率论与数理统计的一个分枝.用于信息处理.信息熵.通信系统.数据传输.率失真理论.密码学.信噪比.数据压缩和相关课题.本文主要罗列一些基于熵的概念及其意 ...

  7. JS一般般的网页重构可以使用Node.js做些什么(转)

    一.非计算机背景前端如何快速了解Node.js? 做前端的应该都听过Node.js,偏开发背景的童鞋应该都玩过. 对于一些没有计算机背景的,工作内容以静态页面呈现为主的前端,可能并未把玩过Node.j ...

  8. poj 2185(二维kmp)

    题意:让你求一个最小的覆盖子矩阵. 分析:首先第一点是确定的:那就是这个子矩阵肯定位于左上角,然后按行考虑,求出每一行可能的重复子串的长度,然后取所有行都存在并且长度最短的长度最为最小子矩阵的宽, 最 ...

  9. ADO.NET+Access: 3,参数 @departmentName 没有默认值

    ylbtech-Error-ADO.NET+Access: 3,参数 @departmentName 没有默认值. 1.A,错误代码返回顶部  3,参数 @departmentName 没有默认值. ...

  10. VB6.0编程笔记——(2)开发环境准备&学习前导入

    工欲善其事必先利其器,着手开始学习写代码之前,我们需要先准备好需要用到的工具.这篇文章会教大家部署好环境,同时会告知前期我们需要知道的一点内容(可以不用特别理解,只要记住用法就行,后续会深入展开介绍) ...