开发者的javascript造诣取决于对【动态】和【异步】这两个词的理解水平。javascript
Promise
技术是【javascript
异步编程】这个话题中很是重要的,它一度让我感到熟悉又陌生,我熟悉其全部的API
并可以在编程中相对熟练地运用,却对其中原理和软件设计思想感到陌生,即使我读了不少源码分析和教程也一度很难理解为何Promise
这样一个普通的类可以实现异步,也曾尝试着去按照Promise/A+规范来编写Promise
,但很快便陷入了一种更大的混乱之中。直到我接触到一些软件设计思想以及软件工程方面的知识后,从代码以外的角度来理解一些细节的必要性时,那些陌生才开始一点点消失。html
若是你以为有些新东西很那理解,有很大的缘由是由于你和设计者所拥有的基础知识储备不是一个水平的,致使你没法理解设计者写出某段代码时所基于的指导思想,当你没法理解某些看起来很复杂的东西时,笔者的建议是先了解它但愿解决的问题,这个问题或许是具体的业务逻辑需求,或许是抽象的软件设计层面的,而后尝试本身想办法去解决它,请永远记得别人是开发者,你也是,你要作的是面向需求,而不只仅是跟着别人走。即时最终你没能开发出这个模块而去学习源码时,你也会发现面对需求而进行的主动思考对此带来的帮助。java
Promise
的本质,是一个分布式的状态机。而PromiseAPI
的本质,就是一个发布订阅模型。node
Promise
解决了什么问题?git
这是一个最基本的问题,Promise
是一个有关可靠性和状态管理的编程范式,它一般被认为从代码层面将javascript
中著名的回调地狱改变成扁平化的写法,并为指定的业务逻辑打上状态标记,让开发者能够更容易地控制代码执行的流程。但事实上Promise
的设计初衷并非为了实现异步,并且不少开发者并无意识到,回调并不意味着异步!!!(你传入另外一个函数的回调函数有可能被异步执行,也有可能被同步执行)。想更好地理解Promise
,就必须把【异步】这个标签从中剥离,而围绕【状态管理】,【可靠性】这些关键词进行展开。github
Promise
只是一个类,为何就可以实现异步?面试
Promise
自己的确只是一个普通的类,并且在不依赖ES6
的环境中,开发者甚至能够手动实现这样一个类,在没有研究Promise
的代码以前,笔者一直主观地认为其内部是经过相似于事件监听的机制来实现异步的,不然程序自己怎么会知道发出的http
请求何时返回结果。编程
这个问题是在笔者学习完EventLoop
和Generator
函数的相关知识后才理解的,其实Promise
自己并无实现异步,javascript
语言中的异步都是经过事件循环的机制(《javascript基础修炼(5)——Event Loop(node.js)》)来实现的,简单地说就是说异步事件的响应是会被事件循环不断去主动检测的,当异步动做知足了再次被执行的条件时(好比http
请求返回告终果,或者在另外一个线程开启的大运算量的逻辑执行完毕后返回了消息),就会被加入调用栈来执行,Promise
和Generator
只是配合事件循环来进行状态管理和流程控制,它们自己和事件循环的机制是解耦的。segmentfault
Promise
做为构造函数调用而生成实例时到底发生了什么事情?promise
这里所指的是下面这样的代码:
promise = new Promise(function(resolve, reject){ //.... });
面试中常常会问到有关Promise
执行次序的问题,不少很是熟悉Promise
用法的读者也并无意识到,实际上传入的匿名函数是会同步执行的。Promise
所作的事情,是为当前这个不知道什么时候能完成的动做打上一些状态的标记,并传入两个用于回收控制权的方法做为参数来启动执行这个匿名函数,经过then
方法指定的后续执行逻辑会先缓存起来(这里的描述并不严谨),当这个异步动做完成后调用resolve
或者reject
方法后,再继续执行事先被缓存起来的流程。
Promise/A+标准看起来很复杂,该如何去实现?
Promise/A+规范的确很复杂,我也不建议你直接就经过这样的方式来了解Promise
的实现细节,【规范】意味着严谨性,也表示其中有不少容错的机制,这会极大地妨碍你对Promise
核心逻辑的理解,Promise
代码最大的复杂性,在于它对于链式调用的支持(若是不须要支持链式调用,你会发现本身几乎不须要思考就能够分分钟撸一个Promise
库出来)。笔者的建议是先想办法去解决主要问题,再对照Promise/A+规范去检视本身的代码。
Promise为何要实现链式调用?
链式调用的实现,实现了Promise
的多步骤流程控制功能,对一个多于两个步骤的流程中,即便没有实现链式调用,Promise
实际上依然能够工做,但当你真的那样作时,你会发现它又变成了一个新的回调地狱。
Promise的可靠性是指什么?
Promise
的可靠性指它的状态只能被改变一次,以后就不能再修改,且惟一修改它的方法是调用promise
实例中的内部resolve( )
或reject( )
方法,它们是定义在Promise
内部的,从外部没法访问到,只能经过Promise
内部提供的机制来触发断定方法(new Promise(executor)
生成实例时,当还行到executor时,Promise会将内部的resolve
和reject
方法做为实参传入executor,从而暴露修改自身状态的能力),相比之下,普通对象的属性或者thenable
对象(指拥有then
方法的非Promise实例对象)的属性都是能够被直接修改的,因此promise
的状态和结果被认为是更可靠的。
假设有一个异步的动做A,还有一个但愿在A完成之后执行的动做B,和一个在B完成之后去执行的动做C,咱们来看一下Promise
是如何实现流程控制。
A动做开始以前,咱们把它丢进Promise
构造函数,Promise
给了A一个控制器(上面有resolve和reject两个按钮)和一个带有两个抽屉的储物柜(onFulfilledCallbacks和onRejectedCallbacks),接着给A交代:我已经登记好信息了,你去执行吧,等你执行完之后,若是你认为执行成功了,就按一下控制器的resolve按钮,若是认为执行失败了就按一下reject按钮,可是你要当心,这个控制器只能用一次,按完它会自动发送消息,储物柜上有接收器,若是收到resolve
信号,onFulfilledCallbacks这个抽屉就会打开,若是收到reject
信号,onRejectedCallbacks这个抽屉就会打开,以后另外一个柜子就会锁死,我每隔一段时间会来查看一下你的状态(注意这里是在事件循环中主动轮询来查看promise
实例是否执行结束的),若是我看到你的储物柜有一个抽屉打开了的话的话,就会把里面的东西拿出来依次执行接下来的事情。在这以前,若是有人想关注你的执行状况的话,我会让它留下两张字条,分别写下不一样的抽屉打开的时须要作的事情,由于最终只有一个抽屉能够打开,他必须得写两张字条,除非他只关注某个抽屉的动向,而后使用你这个储物柜的then
方法就能够把字条塞到对应的柜子里,以后等抽屉打开时,我只须要根据字条上的信息打电话给他就好了。A以为这样是比较稳妥的,因而拿着promise给它的控制器去执行了。
代码继续执行,这时候出现了一个B,B说我得先看看A的执行结果,再决定作什么,执行器说你也别在这干等着了,A在咱们这里存放了一个智能储物柜,它回头会把结果远程发送回来,你把你的联系方式写在这两张字条上,而后经过A的储物柜的then
方法放进去吧,联系方式也能够写成不同的,到时候A返回结果的话,对应的抽屉就会打开,我按照你写的联系方式发消息给你就好了。B想了想也是,因而就写下了两个不一样的号码放进了A储物柜对应的抽屉里,接着就回家睡觉去了。
代码继续执行,这时候又出现了一个C,C说我想等B返回结果之后再执行,这时候执行器犯难了,B还没出发呢,我也没有给它分配回调储物柜,因此没办法用一样的方式对待C,执行器只能对C说,咱们这规定若是没有对应标记的储物柜的话,暂时不提供服务,这样吧,你先把你的联系方式写好交给我,等回头若是B出发的话,我会给它分派储物柜,到时候把你的需求放在对应的抽屉里,等B返回对应结果之后我再通知你,C以为也行,因而就照作了。可是C走后,执行器就想了,要是后面再来DEF都要跟在不一样的人后面去执行,那这些事情我都得先保管着,这也太累了,并且容易搞乱,不能这么搞啊。
上一会讲到在现有机制下缺少多步骤流程管理的机制,当异步任务A执行且没有返回结果时,后续全部的动做都被暂存在了执行器手里,只能随着时间推移,当标志性事件发生时再逐步去分发事件。为了可以实现多步骤的流程管理,执行器想出了一个方法,为每个来注册后续业务逻辑的人都提供一个智能储物柜,这样在办理登记时就能够直接将后续的方法分发到对应的抽屉里,常见的问题就解决了。
若是没有链式调用,第三节中的多步骤的伪代码多是以下的样子:
//为了聚焦核心逻辑,下面的伪代码省略了onReject的回调 promiseA = new Promise(function(resolve, reject){ //A带着控制器开始执行 A(resolve,reject); }); promiseA.then(function(resA){ //A执行结束之后,开始判断B究竟是否要执行 promiseB = new Promise(function(resolveB, rejectB){ //若是B须要执行,则分配两个储物柜,并派发状态控制器,B带着A返回的数据resA开始执行 B(resA,resolveB,rejectB); }); promiseB.then(function(resB){ //B执行结束之后,开始判断C究竟是否要执行 promiseC = new Promise(function(resolveC, rejectC){ //若是C须要执行,则分配两个储物柜,并派发状态控制器,C带着B返回的数据resB开始执行 C(resB, resolveC, rejectC); }); //...若是有D的话 }) });
在逻辑流程中仅仅有3个步骤的时候,回调地狱的苗头就已经显露无疑了。Promise
被设计用来解决回调嵌套过深的问题,若是只能按上面的方法来使用的话显然是不能知足需求的。若是能够支持链式调用,那么上面代码的编写方式就变成了:
//为了聚焦核心逻辑,下面的伪代码省略了onReject的回调 promiseA = new Promise(function(resolve, reject){ //A带着控制器开始执行 A(resolve,reject); }); promiseA.then(function(resA){ //在使用then方法向A的储物柜里存放事件的同时,也生成了本身的储物柜 return new Promise(function(resolveB, rejectB){ B(resA, resolveB, rejectB); }); }).then(function(resB){ return new Promise(function(resolveC, rejectC){ C(resB, resolveC, rejectC); }); }).then(function(resC){ //若是有D动做,则继续 })
很明显,当流程步骤增多时,支持链式调用的方法具备更好的扩展性。下一节讲一下Promise
最关键的链式调用环节的实现。
若是须要then
方法支持链式调用,则Promise.prototype.then
这个原型方法就须要返回一个新的promise
。事实上即便在最初的时间节点上来看,后续注册的任务也符合在将来某个不肯定的时间会返回结果的特色,只是多了一些前置条件的限制。返回新的promise
实例是很是容易作到的,但从代码编写的逻辑来理解,这里的promise
究竟是什么意思呢?先看一下基本实现的伪代码:
//为简化核心逻辑,此处只处理Promise状态为PENDING的状况 //同时也省略了容错相关的代码 Promise.prototype.then = function(onFulfilled, onRejected){ let that = this; return new Promise(function(resolve, reject){ //对onFulfilled方法的包装和归类 that.onFulfilledCallbacks.push((value) => { let x = onFulfilled(value); someCheckMethod(resolve, x, ...args); }); //对onRejected方法的包装和归类 that.onRejectedCallbacks.push((reason) => { let x = onRejected(reason); someCheckMethod(reject, x, ...args); }); }); };
能够看到在支持链式调用的机制下,最终被添加至待执行队列中的函数并非经过then
方法添加进去的函数,而是经过Promise
包装为其增长了状态信息,而且将这个状态改变的控制权交到了onFulfilled
函数中,onFulfilled
函数的返回结果,会做为参数传入后续的断定函数,进而影响在执行resolve
的执行逻辑,这样就将新promise
控制权暴露在了最外层。
因此,then方法中返回的promise实例,标记的就是添加进去的
onFulfilled
和onRejected
方法的执行状态。这里的关键点在于,onFulfilled
函数执行并返回结果后,才会启动对于这个promise的决议。
在新的链式调用的支持下,上面的故事流程就发生了变化。当B前来登记事件时,执行器说咱们这如今推出了一种委托服务,你想知道那个储物柜的最新动态,就把你的电话写在字条上放在对应的抽屉里,以后当这个抽屉打开后,咱们就会把它返回的信息发送到你留在字条上的号码上,咱们会给你提供一个智能储物柜(带有this._onFulfillCallbacks
抽屉和this._onRejectedCallbacks
抽屉)和一个控制器,这样别人也能够关注你的动态,但你的控制器暂时不能用,咱们将某个消息发送到你留的手机号码上时,才会同步激活你的控制器功能,但它也只能做用一次。
再来考虑一种特殊的场景,就是当A动做调用resolve(value )
方法来改变状态机的状态时,传入的参数仍然是一个PENDING
状态的promise
,这至关于A说本身已经完成了,可是此时却没法获得执行结果,也就不可能将结果做为参数来启动对应的apromise._onFulfilledCallbacks
队列或者apromise_onRejectedCallbacks
队列,此时只能先等着这个promise
改变状态,而后才能执行对A动做的决议。也就是说A的决议动做要延迟到这个新的promise
被决议之后。用伪代码来表示这种状况的处理策略就是以下的样子:
//内部方法 let that = this;//这里的this指向了promise实例 function resolve(result){ if(result instanceof Promise){ return result.then(resolve, reject); } //执行相应的缓存队列里的函数 setTimeout(() => { if (that.status === PENDING) { that.status = FULFILLED; that.value = result; that.onFulfilledCallbacks.forEach(cb => cb(that.result)); } }); }
当前promise
实例的决议经过result.then(resolve,reject)被推迟到result返回结果以后,而真正执行时所须要操做的对象和属性,已经经过let that = this与实例进行了绑定 。
不少开发者在这里会以为很是混乱,极可能是没有意识到每个promise
实例都会生成内部方法resolve( )
和reject( )
,即时当Promise
类实例化的过程结束后,它们依然会被保持在本身的闭包做用域中,在执行栈中涉及到多个处于PENDING
状态的promise
时,它们的内部方法都是存活的。若是仍是以为抽象,能够利用Chrome的调试工具,将下面的代码逐步执行,并观察右侧调用栈,就能够看到当传入决议函数的是另外一个promise
时,外层的决议函数都会以闭包的形式继续存在。
let promise1 = new Promise(function(resolve, reject){ setTimeout(function fn1(){ let subpromise = new Promise(function (resolvesub,rejectsub) { setTimeout(function fn2() { resolvesub('value from fn2'); },2000); }); resolve(subpromise); },2000); }); promise1.then(function fn3(res) { console.log(res); });
【Promise/A+规范】:https://github.com/promises-aplus/promises-spec
理清了上面各类状况的基本策略后,咱们已经具有了构建一个相对完备的Promise
模块的能力。我强烈建议你按照Promise/A+规范来亲自动手实现一下这个模块,你会发如今实现的过程当中仍然有大量的代码层面的问题须要解决,但你必定会受益于此。网上有很是多的文章讲述如何根据Promise/A+标准来实现这个库,但是在笔者看来这并非什么值得炫耀的事情,就好像对照着攻略在打游戏同样。
做为工程师,你既要可以一行一行写出这样一个模块,更要关注规范为何要那样规定。
【Promise/A+测试套件】: https://github.com/promises-aplus/promises-tests
若是你对照规范的要求写出了这个模块,能够利用官方提供的测试套件(包含800多个测试用例来测试规范中规定的各个细节)来测试本身编写的模块并完善它。javascript语言中都是经过鸭式辩型来检测接口的,不管你是怎样实现规范的各个要求,只要最终经过测试套件的要求便可。若是你依旧以为内心没谱,也能够参考别人的博文来学习Promise
的细节,例如这篇《Promise详解与实现》就给了笔者很大帮助。
当越过了语言层面的难点后,推荐你阅读《深刻理解Promise五部曲》这个系列的文章。大多数开发者对于Promise
的理解和应用都是用来解决回调地狱问题的,而这个系列的文章会让你从另外一个角度从新认识Promise,不得不说文章中用发布订阅模式来类比解释Promise
的实现机制对于笔者理解Promise提供了巨大的帮助,同时它也可以引起一些经过学习promise/A+规范很难意识到的关于精髓和本质的思考。