4月阅读周·你不知道的JavaScript | 异步流控制,跨越同步和异步边界,对行为规范化
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读三个月。
4月份的阅读计划有两本,《你不知道的JavaScrip》系列迎来收尾。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》。
当前阅读周书籍:《你不知道的JavaScript(下卷)》。
异步流控制
Promise
Promise可以被看作是同步函数返回值的异步版本。
Promise的决议结果只有两种可能:完成或拒绝,附带一个可选的单个值。如果Promise完成,那么最终的值称为完成值;如果拒绝,那么最终的值称为原因(也就是“拒绝的原因”)。Promise只能被决议(完成或者拒绝)一次。之后再次试图完成或拒绝的动作都会被忽略。因此,一旦Promise被决议,它就是不变量,不会发生改变。
构造和使用Promise
可以通过构造器Promise(..)构造promise实例:
var p = new Promise(function (resolve, reject) {
// ..
});
提供给构造器Promise(..)的两个参数都是函数,一般称为resolve(..)和reject(..)。
它们是这样使用的:
- 如果调用reject(..),这个promise被拒绝,如果有任何值传给reject(..),这个值就被设置为拒绝的原因值。
- 如果调用resolve(..)且没有值传入,或者传入任何非promise值,这个promise就完成。
- 如果调用resove(..)并传入另外一个promise,这个promise就会采用传入的promise的状态(要么实现要么拒绝)——不管是立即还是最终。
Promise有一个then(..)方法,接受一个或两个回调函数作为参数。前面的函数(如果存在的话)会作为promise成功完成后的处理函数。第二个函数(如果存在的话)会作为promise被显式拒绝后的处理函数,或者在决议过程中出现错误/异常的情况下的处理函数。
Thenable
Promise(..)构造器的真正实例是Promise。但还有一些类promise对象,称为thenable,一般来说,它们也可以用Promise机制解释。
任何提供了then(..)函数的对象(或函数)都被认为是thenable。Promise机制中所有可以接受真正promise状态的地方,也都可以处理thenable。
从根本上说,thenable就是所有类promise值的一个通用标签,这些类promise不是被真正的Promise(..)构造器而是被其他系统创造出来。从这个角度来说,通常thenable的可靠性要低于真正的Promise。举个例子,考虑下面这个胡作非为的thenable:
var th = {
then: function thener(fulfilled) {
// 每100ms调用一次fulfilled(..),直到永远
setInterval(fulfilled, 100);
},
};
如果接收到了这个thenable,并通过th.then(..)把它链接起来,很可能你会吃惊地发现自己的完成处理函数会被重复调用,而正常的Promise应该只会决议一次。
Promise API
Promise API还提供了一些静态方法与Promise一起工作。
Promise.resolve(..)创建了一个决议到传入值的promise。我们把它的工作机制与手动方法对比一下:
var theP = ajax( .. );
var p1 = Promise.resolve( theP );
var p2 = new Promise( function pr(resolve){
resolve( theP );
} );
Promise.reject(..) 创建一个立即被拒绝的promise,和与它对应的Promise(..)构造器一样:
var p1 = Promise.reject('Oops');
var p2 = new Promise(function pr(resolve, reject) {
reject('Oops');
});
resolve(..)和Promise.resolve(..)可以接受promise并接受它的状态/决议,而reject (..)和Promise.reject(..)并不区分接收的值是什么。所以,如果传入promise或thenable来拒绝,这个promise / thenable本身会被设置为拒绝原因,而不是其底层值。
Promise.all([ .. ]) 接受一个或多个值的数组(比如,立即值、promise、thenable)。它返回一个promise,如果所有的值都完成,这个promise的结果是完成;一旦它们中的某一个被拒绝,那么这个promise就立即被拒绝。
Promise.all([p1, p2, v3]).then(function fulfilled(vals) {
console.log(vals); // [42,43,44]
});
Promise.all([p1, p2, v3, p4]).then(
function fulfilled(vals) {
// 不会到达这里
},
function rejected(reason) {
console.log(reason); // Oops
},
);
Promise.all([ .. ]) 等待所有都完成(或者第一个拒绝),而Promise.race([ .. ])等待第一个完成或者拒绝。
生成器 + Promise
可以把一系列promise以链式表达,用以代表程序的异步流控制。
还有一种更好的方案可以用来表达异步流控制,用生成器来表达异步流控制。
生成器可以yield一个promise,然后这个promise可以被绑定,用其完成值来恢复这个生成器的运行。
用生成器来表达前面代码片段的异步流控制:
function* main() {
var ret = yield step1();
try {
ret = yield step2(ret);
} catch (err) {
ret = yield step2Failed(err);
}
ret = yield Promise.all([step3a(ret), step3b(ret), step3c(ret)]);
yield step4(ret);
}
为什么与生成器一起使用Promise?
Promise是一种把普通回调或者thunk控制反转,反转回来的可靠系统。因此,把Promise的可信任性与生成器的同步代码组合在一起有效解决了回调所有的重要缺陷。另外,像Promise.all([ .. ])这样的工具也是在生成器的单个yield步骤表达并发性的一个优秀又简洁的方法。
这个魔法是如何实现的呢?我们需要一个可以运行生成器的运行器(runner),接受一个yield出来的promise,然后将其连接起来用以恢复生成器,方法是或者用完成成功值,或者用拒绝原因值抛出一个错误到生成器。
很多支持异步的工具/库都有这样的“运行器”,比如Q.spawn(..),以及我的asyncquence库的runner(..)插件。而这里是用一个单独的运行器来说明这个过程的工作原理:
function run(gen) {
var args = [].slice.call(arguments, 1),
it;
it = gen.apply(this, args);
return Promise.resolve().then(function handleNext(value) {
var next = it.next(value);
return (function handleResult(next) {
if (next.done) {
return next.value;
} else {
return Promise.resolve(next.value).then(handleNext, function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleResult);
});
}
})(next);
});
}
总结
我们来总结一下本篇的主要内容:
- ES6新增了Promise来弥补回调的主要缺陷之一:缺少对可预测行为方式的保证。Promise代表了来自于可能异步的任务的未来完成值,跨越同步和异步边界对行为进行规范化。
- Promise与生成器的结合完全实现了重新安排异步流控制代码来消除丑陋的回调乱炖(或称“地狱”)。
- 可以在各种异步库运行器的帮助下管理这些交互,而JavaScript最终会提供专门的语法来支持这种交互。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)