简介
一个 Promise
就是一个对象,它代表了一个异步操作的最终完成或者失败。大多数人仅仅是使用已创建的Promise
实例对象,因此本教程将首先说明怎样使用 Promise
,之后说明如何创建Promise
。
本质上,Promise
是一个绑定了回调的对象,而不是将回调传进函数内部。
原生提供了Promise
对象。本篇不注重讲解promise
的用法,关于用法,可以看阮一峰老师的ECMAScript 6
系列里面的Promise
部分:
ECMAScript 6 : Promise 对象
我们在最后实现一个es2015
版本的PromiseA
.
注:在本代码中有很多不完善的地方,最后会给出一个es2015
的版本,那个版本是比较完善的。
本篇博客逐步实现,最终使其符合Promises/A+
规范
代码实现
逐步实现:
- 基础版本
- 支持同步任务
- 支持状态
- 支持链式操作
- 支持串行异步任务
- 实现 Promise 方法 all、resolve、reject、race 等方法
- 实现 promiseify 方法
- 达到 PromiseA+规范
注意事项:这边建议不要使用
setTimeout
作为Promise
的实现。因为setTimeout
属于 宏任务, 而Promise
属于 微任务。
显示代码请看
基础版本(异步回调)
目标
- 可以通过
new
关键字创建一个Promise
实例。 Promise
实例传入的异步方法执行成功就执行注册的成功回调函数,失败就执行注册的失败回调函数。
首先实现两个判断函数:
1 | // 判断当前传入的参数是否是function |
注:判断传入参数是否为function
, 根据当前环境降级实现微任务或宏任务
代码实现
1 | // 判断当前传入的参数是否是function |
判断实例传入的参数是否为function
,在then
中注册了这个promise
实例的成功回调和失败回调,当promise resolve
时,当promise resolve
时,就把异步执行结果赋值给promise
实例的value
,并把这个值传入成功回调中执行,失败就把异步执行失败原因赋值给promise
实例的error
,并把这个值传入失败回调并执行。
支持同步代码
我们执行
1 | new PromiseA((resolve, reject) => { |
现在如果我们同步执行resolve(111)
的话,我们的then
函数还没有被执行,所以后续的then
中得回调函数也不会被执行,简单来说就是then
函数在resolve(111)
的函数之后执行,所以then
中得回调也不会被执行。
目标
- 使
promise
支持同步方法
代码
1 | // 判断当前传入的参数是否是function |
就是在resolve
和reject
里面用setTimeout
进行包裹,使其到then
方法执行之后再去执行,这样我们就让Promise
支持传入同步方法。
注:
setTimeout
其实是最后一种方法,要看环境, 要是支持的话,优先使用MutationObserver
(微任务),再是MessageChannel
(优先级比定时器高的宏任务),再是setImmediate
(这个兼容性太差,不建议用)再不行就降级为setTimeout
了
支持三种状态
我们知道在使用Ppromise
时,Promise
有三种状态:pending(进行中)
、fulfilled(已成功)
、rejected(已失效)
。
- 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise
对象的状态改变,
只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)
。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。
目标
- 实现
promise
的三种状态 - 实现
promise
对象的状态改变,改变只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。 - 实现一旦
promise
状态改变,再对promise
对象添加回调函数,也会立即得到这个结果。
代码实现
1 | // 判断当前传入的参数是否是function |
为了实现上面的目标我们建立了三种状态:pending
、fulfilled
、rejected
,如果是peding
状态,我们才会改变promise
的状态,并且执行相关状态的操作,并且现在的promise
的状态是不可改变的。在then
那种我们判断promise
的状态已经从pending
转换为fulfilled
或者rejected
就会立刻执行他的状态的回调,并且把结果传入。
支持链式调用(同步)
大家都知道jquery
的链式调用,promise
也是支持链式调用。
我们首先在这一步实现同步的链式调用。
目标
- 使
promise
支持链式调用
注:我们把
then
中的回调存入数组中
1 | self.onFulfilledCallbacks = []; |
当我们执行回调时,也要改成遍历回调数组执行回调函数
1 | self.onFulfilledCallbacks.forEach((callback) => callback(self.value)); |
最后,then
方法也要改一下,只需要在最后一行加一个return this
即可,这其实和jQuery
链式操作的原理一致,每次调用完方法都返回自身实例,后面的方法也是实例的方法,所以可以继续执行。
代码实现
1 | // 判断当前传入的参数是否是function |
总结: 这个就是最简单的同步then
的回调用一个内部数组来储存,最后循环调用。
支持串行异步任务
我们一般都是用promise.then
来写异步任务,在下面完善一下代码
目标
- 使
PromiseA
支持串行异步操作 - 支持传入
PromiseA
对象
实现
在上一步已经实现可以链式调用,但是只支持同步的链式调用,现在要实现支持一步串行调用。
代码如下:
1 | // 第一个promise实例 |
想要的结果是在1s之后输出'first------' + new Date()
结果,再经过2s之后执行'second------' + new Date()
,再等3s之后才会执行'third------' + new Date()
,但是结果和预想的结果不相同。
实际结果是1s之后直接就会输出三次first------ + new Date()
,因为所有的回调函数都注册在了PromiseA
中的onFulfilledCallbacks
队列里,在后面resolve
后会全部执行,这个并不能满足异步串行。
需要将每个回调函数注册在对应promise
实例的onFulfilledCallbacks
里面,然后再返回一下新的promise
以做到异步串行效果。
改写代码如下:
- 改写
prototype
上的then
方法 - 改写
resolve
、reject
上的代码
Promise.prototype.then
1 | // 修改原型上的then方法 |
接着修改 resolve
和 reject
:依次执行队列中的函数
当 resolve
或 reject
方法执行时,依次提取成功或失败任务队列当中的函数开始执行,并清空队列,从而实现 then
方法的多次调用,实现的代码如下:
1 | function callAsync(fn, arg, callback, onError) { |
在这里面最关键的就是回调函数返回异步 PromiseA 对象时,要等异步 PromiseA 对象有结果时,当前的实例才能根据前面的异步结果改变自己的状态。
测试一下代码,在第一PromiseA
实例的then
的回调函数中,返回一个新的PromiseA
实例并且在2s
之后直接执行当前实例的resolve
方法,在下一个then
回调函数中再返回一个PromiseA
实例,在3s
后执行当前实例的resolve
,最后一个then
传入的回调函数中输出resolve
中储存的值。
1 | // p1 |
整理一下内部的执行过程:
- 当实例化
p1
时,在它1s
后调用resolve
函数,在它的t1
中传入一个函数,这个函数返回一个p2
实例。 t2
的执行,要等到p2
的状态改变才执行。当执行t2
时,又会产生一个p3
实例。- 等到
p3
的状态改变时,才会触发后续的t3
执行。