You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Aug 7, 2024. It is now read-only.
// 执行 promise 之后所有 then 的函数functionpublish(promise){letsubscribers=promise._subscribers;letsettled=promise._state;// 如果后面没有 then 就直接返回if(subscribers.length===0){return;}letchild,callback,detail=promise._result;// 每次注册 then 的时候,都是往 _subscribers 添加 promise 和两个回调函数,所以 +3:for(leti=0;i<subscribers.length;i+=3){child=subscribers[i];callback=subscribers[i+settled];if(child){// 如果被 then 了,则执行子 promise 注册的回调函数invokeCallback(settled,child,callback,detail);}else{// 如果后面没有 then 了,则可以直接执行回调callback(detail);}}promise._subscribers.length=0;}
当到了 invokeCallback,实际上就是执行每个子 promise 的回调,这时会有几种情况:1. 如果注册的回调函数的确实是函数,则执行回调函数得到结果,如果没有返回错误则说明要 fulfill 这个 promise。2. 如果注册的回调函数却不是函数,则说明发生了值穿透,此时直接用父 promise 的 result 来作为子 promise 的 result 向下传递。
前言
本文适合有一定 Promise 使用基础的读者,如果你还没有使用过 Promise,建议先通过以下教程了解 Promise 的使用方法
本篇文章中我们要分析的 Promise 实现库的源码是 es6-promise,一个通过了 Promises/A+ Tests 的 polyfill,源码通过 ES6 书写比较简洁。为方便理解,放上我已经写了注释的仓库地址:es6-promise-annotated,可以配合阅读。
源码分析
思路
在正式进入源码分析之前,我们先明确 Promise 的一些基本准则:
new Promise()
自然都会返回一个 Promise 实例对象,调用.then()
Promise.resolve()
Promise.reject()
返回的也都是一个被包装好的 Promise 对象。Promise.prototype.then
和Promise.prototype.catch
的参数中的回调函数虽然是异步执行的,但是向 promise 对象注册执行成功时和失败时相应的回调函数的行为是同步执行的。Promise 的工作主要分为两个阶段,一是同步注册,二是异步触发链式调用。
同步注册
一般来说在调用 Promise 时都是多个 Promise 对象串联起来的,then 中函数的调用是异步的, 但是 Promise 对象的创建是同步的,如下例子中:
p1 和 p1 被 then 包装出来的 Promise 对象是会在整个 script 脚本的执行过程中同步完成,这个过程就是注册,每个子 promise 都会添加到父 promise 的属性上来完成注册,以便在接下来的链式调用中按照顺序执行。
链式调用
Promise 可以链式调用关键的一点就是确定好每个节点的调用接口(有点像递归的递归入口),这样每个 Promise 对象就可以通过相同的接口串联起来调用,Promise 对象的模型如下图:
每个父 promise 传递给子 promise 们它的 settled 状态来指示该触发子 promise 成功或失败的回调函数,并传递给子 promise 它的 settled 值。
构造函数
每个 Promise 对象都有三个状态:
pending
fulfilled
rejected
,所以需要有一个内部属性来标识状态。向 Promise 对象注册的回调函数也会被同步存在一个内部属性中,在执行子 promise 的时候取出进行异步调用。Promise 的构造函数只接受一个参数,函数签名为
function(resolve, reject){}
,所以要对传入的参数进行判断,传入的参数非函数时直接报错(如果想传入非函数的参数要使用Promise.resolve
,会自动包装出一个 Promise 对象,后面也会讲到)。Promise 构造函数中的函数是同步执行的,通过 initializePromise 来启动这个 Promise 对象
但我们知道
new Promise(function(resolve, reject))
中 resolve 和 reject 的执行是异步的,value 这个参数就是在 Promise 的构造函数中传递给 resolve 和 reject 的参数值(可以看到,resolve 和 reject 都只能接受一个参数,后面的参数会被直接忽略)。到这一步,回调的
resolve(value)
和reject(error)
实际上在内部被封装成了(value) => resolve(promise, value)
和(reason) => reject(promise, reason)
,promise 就是当前的 Promise 对象,value 在这里就是'a'
,reject 同理。表面看上去只是多了一个 promise 的参数,但是子 promise 事先已经向父 promise 通过 then 完成了同步的注册,启动之后,就能异步的链式调用了。在这里要注意 resolve 和 reject 是异步的,后面会讲到。
注册
接下来我们来看下
then
和catch
是如何在 Promise 上注册回调的。then 中传入的一个或两个函数都会被包装成 Promise 对象来满足 Promises/A+ 规定,并且只有返回 Promise 对象才可以符合调用的接口。每次 then 的 Promise 对象(子 promise)会连同它的 resolve 和 reject 回调函数一同存入被 then 的 Promise 对象(父 promise)的
_subscribers
中。链式调用的实现
在上一节中,then 的 Promise 对象向被 then 的 Promise 对象注册了回调事件,至此完成了第一阶段 —— 同步注册。接下来是第二个阶段 —— 启动链式调用。
Promise 有几种创建的方法:1. 通过构造函数 new 出来的。2. 通过 then 或 reject 出来的 3. 通过 Promise.resolve 或 Promise.reject 创建的。这三种创建 Promise 对象的启动的方法相同,不同的是
分别分情况来看:
构造函数 new 出来的 Promise
对于通过构造函数创建的 Promise 对象,没有父 promise 来给它 settled 状态及返回值,这两者都是在传给 Promise 的函数的逻辑中传递的。
then 或 reject 出来的 Promise 对象
对于 then 或 reject 出来的 Promise 对象,要先包装出一个 Promise 对象
然后根据父 proimse 的状态来确定自己将要执行 onFulfilled 或 onRejected,如果父 promise 已经 settled 了,则可以直接执行 then 的回调函数。
如果父 promise 还没有 settled,就默默进行注册,上面已经提到过,subscribe 函数会在第一个次注册回调函数时执行所有父 promise 的回调函数。
Promise.resolve 或 Promise.reject 返回的 Promise
拿 Promise.resolve 来举例即可,Promise.reject 同理。
这里的 _resolve 就是之前出现多次的 resolve,表示当前 Promise 已走 onFulfilled 的回调了。
确定 promise 的状态
当一个 Promise 对象接受父 promise 传入的状态或根据自身的回调函数确定要执行 onFulfilled 的回调函数时可能遇到三种情况:1. 如果传入 onFulfilled 的参数是自身,会导致递归爆栈,这时要 reject 掉。 2. 传入的回调函数是一个 thenable 对象,那么 3. 如果传入的是其他对象,则可以直接 fulfill 掉当前的 Promise 对象。
fulfill 函数的功能就是确定当前 promise 的 state 和 result。之后,就可以通过 asap 异步通知所有子 promise 开始执行。
对于 publish,就是依次同步处理所有子 promise 们。如果子 promise 有注册了回调,那么触发子 promise。
当到了 invokeCallback,实际上就是执行每个子 promise 的回调,这时会有几种情况:1. 如果注册的回调函数的确实是函数,则执行回调函数得到结果,如果没有返回错误则说明要 fulfill 这个 promise。2. 如果注册的回调函数却不是函数,则说明发生了值穿透,此时直接用父 promise 的 result 来作为子 promise 的 result 向下传递。
异步执行的实现
我们知道 Promise 处于的异步队列是 microTask,这里不再重复 task 和 mircoTask 的执行顺序,说一下 microTask 的意义,引用 顾轶灵 大神在这个问题下回答:
microTask 某种程度上来说就是 task 执行前的钩子函数,JS 引擎在执行完一个 task 后会更新 UI,有了 microTask 就能在改变 UI 改变前操作 DOM。
对于简易实现的 Promise,一般都是直接使用 setTimeout 来做延迟,但是 setTimeout 属于 marcotask,在 es6-promise 中按顺序使用以下方式来进行异步的延迟,优先使用可以使用的方法。
nextTick
这是 Node 中特有的 microTask 函数,在 Node 环境中直接使用它即可。
MutationObserver
MutationObserver 是 HMLT5 引入的能在某个范围内的DOM树发生变化时作出适当反应的能力.该API设计用来替换掉在DOM3事件规范中引入的Mutation事件.
在调用时创建一个 BrowserMutationObserver 来监视一个 node,回调函数为需要异步执行的回调函数。
MessageChannel
同理,对其中一个通道随便发送一个消息,另一个通道执行回调即可。
另外
所以 MessageChannel 可以作为 MutationObserver 的替补及 Web Worker 中的异步方法。
vert.x
笔者之前从来没听说个这个东西,找到了官网,就不在这里过多研究了。
setTimeout
如果以上方法皆不行则最后采用 setTimeout 的方法来执行。但是使用 setTimeout 来进行异步操作的话就不再是 microTask 的 Promise 了。
race
race 的实现比较简单,因为 promise 只能被 settle 一次,所以直接对 race 中传递的 promise 们都 then 上 race 的回调函数即可,回调函数会被最先完成的 promise å给 settle,。
all
在内部维护一个保存结果的数组及记录未 settled 的 promise 的数量值,在每一次 settled 的时候都将这个值减1,当最后一个 promise settled 之后,记录结果的数组也都保存好了每个 promise 的返回值,可以触发包装的 promise 的回调了。
总结
可以看到,一个可以用于生产环境中的 Promise 库,对各种边界条件都有很好的处理,并且能兼容第三方的 thenable 对象,选用合适的 API 来实现将任务推到 microtask 中。不过,做的更好的 Promise 库还应该有很强的拓展性,比如 bluebird,但是,通过了解 es6-promise 对梳理 Promise 中的各种非常规操作还是大有好处。
The text was updated successfully, but these errors were encountered: