带你熟悉Promise
Promise/A+ 规范
在编写 Promise 之前,我们必须了解 Promise/A+ 规范。由于内容较长,下面我总结了几点,更详细的内容可以查阅 Promise/A+ 规范。
Promise 是一个对象或者函数,对外提供了一个 then 函数,内部拥有 3 个状态。
then 函数
then 函数接收两个函数作为可选参数:
promise.then(onFulfilled, onRejected)
同时遵循下面几个规则:
- 如果可选参数不为函数时应该被忽略;
- 两个函数都应该是异步执行的,即放入事件队列等待下一轮 tick,而非立即执行;
- 当调用 onFulfilled 函数时,会将当前 Promise 的值作为参数传入;
- 当调用 onRejected 函数时,会将当前 Promise 的失败原因作为参数传入;
- then 函数的返回值为 Promise。
Promise 状态
Promise 的 3 个状态分别为 pending、fulfilled 和 rejected。
- pending:“等待”状态,可以转移到 fulfilled 或者 rejected 状态
- fulfilled:“执行”(或“履行”)状态,是 Promise 的最终态,表示执行成功,该状态下不可再改变。
- rejected:“拒绝”状态,是 Promise 的最终态,表示执行失败,该状态不可再改变。
Promise 解决过程
Promise 解决过程是一个抽象的操作,即接收一个 promise 和一个值 x,目的就是对 Promise 形式的执行结果进行统一处理。需要考虑以下几种情况。
情况 1: x 等于 promise
抛出一个 TypeError 错误,拒绝 promise。
情况 2:x 为 Promise 的实例
如果 x 处于等待状态,那么 promise 继续等待至 x 执行或拒绝,否则根据 x 的状态执行/拒绝 promise。
情况 3:x 为对象或函数
该情况的核心是取出 x.then 并调用,在调用的时候将 this 指向 x。将 then 回调函数中得到结果 y 传入新的 Promise 解决过程中,形成一个递归调用。其中,如果执行报错,则以对应的错误为原因拒绝 promise。
这一步是处理拥有 then() 函数的对象或函数,这类对象或函数我们称之为“thenable”。注意,它只是拥有 then() 函数,并不是 Promise 实例。
情况 4:如果 x 不为对象或函数
以 x 作为值,执行 promise。
Promise 实现
下面我们就根据规范来逐步实现一个 Promise。
Promise() 函数及状态
由于 Promise 只有 3 个 状态,这里我们可以先创建 3 个“常量”来消除魔术字符串:
var PENDING = 'pending' var FULFILLED = 'fulfilled' var REJECTED = 'rejected
由于 Promise 可以被实例化,所以可以定义成类或函数,这里为了增加难度,先考虑在 ES5 环境下实现,所以写成构造函数的形式。
使用过 Promise 的人肯定知道,在创建 Promise 的时候会传入一个回调函数,该回调函数会接收两个参数,分别用来执行或拒绝当前 Promise。同时考虑到 Promise 在执行时可能会有返回值,在拒绝时会给出拒绝原因,我们分别用 value 和 reason 两个变量来表示。具体代码如下:
function Promise(execute) { var self = this; self.state = PENDING; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; } } try { execute(resolve, reject); } catch (e) { reject(e); } }
Promise 是单次执行的,所以需要判断状态为 PENDING 的时候再执行函数 resolve() 或函数 reject() 。同时 Promise 的内部异常不能直接抛出,所以要进行异常捕获。
then() 函数
每个 Promise 实例都有一个 then() 函数,该函数会访问 Promise 内部的值或拒绝原因,所以通过函数原型 prototype 来实现。then() 函数接收两个回调函数作为参数,于是写成下面的形式:
Promise.prototype.then = function (onFulfilled, onRejected) { }
根据第 1 条原则,如果可选参数不为函数时应该被忽略,所以在函数 then() 内部加上对参数的判断:
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : function (x) { return x }; onRejected = typeof onRejected === "function" ? onRejected : function (e) { throw e };
根据第 2 条规则,传入的回调函数是异步执行的。要模拟异步,可以通过 setTimeout 来延迟执行。再根据第 3 条和第 4 条规则,应根据 Promise 的状态来执行对应的回调,执行状态下调用 onFulfilled() 函数,拒绝状态下调用 onRejected() 函数。
var self = this; switch (self.state) { case FULFILLED: setTimeout(function () { onFulfilled(self.value); }) break; case REJECTED: setTimeout(function () { onRejected(self.reason); }) break; case PENDING: // todo break; }
等待状态下就有些麻烦了,需要等到 Promise 状态转变时才能调用。
按照常规处理方式,可以建立一个监听,监听 Promise 的状态值改变。由于浏览器环境和 Node.js 环境的事件监听不一样,考虑兼容性,这种实现会比较复杂。
换个角度来看,在不考虑异常的情况下 Promise 的状态改变只依赖于构造函数中的 resolve() 函数和 reject() 函数执行。所以可考虑将 onFulfilled() 和 onRejected() 函数先保存到 Promise 属性 onFulfilledFn 和 onRejectedFn 中,等到状态改变时再调用。
case PENDING: self.onFulfilledFn = function () { onFulfilled(self.value); } self.onRejectedFn = function () { onRejected(self.reason); } break;
最后看第 5 条规则,then() 被调用时应该返回一个新的 Promise,所以在上面的 3 种状态的处理逻辑中,都应该创建并返回一个 Promise 实例。以执行状态为例,可以改成下面的样子。
case FULFILLED: promise = new Promise(function (resolve, reject) { setTimeout(function () { try { onFulfilled(self.value); } catch (e) { reject(e) } }) }); break;
同时,它带来的另一个效果是支持链式调用。在链式调用的情况下,如果 Promise 实例处于等待状态,那么需要保存多个 resolve() 或 reject() 函数,所以 onFulfilledFn 和 onRejectedFn 应该改成数组。
case PENDING: promise = new Promise(function (resolve, reject) { self.onFulfilledFn.push(function () { try { onFulfilled(self.value); } catch (e) { reject(e) } }); self.onRejectedFn.push(function () { try { onRejected(self.reason); } catch (e) { reject(e) } }) }); break;
对应的,Promise 构造函数中应该初始化属性 onFulfilledFn 和 onRejectedFn 为数组,同时 resolve() 和 reject() 函数在改变状态时应该调用这个数组中的函数,并且这个调用过程应该是异步的。
function Promise(execute) { ... self.onFulfilledFn = []; self.onRejectedFn = []; ... function resolve(value) { setTimeout(function() { ... self.onFulfilledFn.forEach(function (f) { f(self.value) }) }) } function reject(reason) { setTimeout(function() { ... self.onRejectedFn.forEach(function (f) { f(self.reason) }) }) } }
resolvePromise() 函数
前面提到解决过程函数有两个参数及 3 种情况,先来考虑第 1 种情况,promise 与 x 相等,应该直接抛出 TypeError 错误:
function resolvePromise(promise, x) { if (promise === x) { return reject(new TypeError("x 不能与 promise 相等")); } }
情况 2,x 为 Promise 的实例,应该尝试让 promise 接受 x 的状态,怎么接受呢?
直接改变 promise 状态肯定是不可取的,首先状态信息属于内部变量,其次也无法调用属性 onResolvedFn 和 onFulfilledFn 中的待执行函数。所以必须要通过调用 promise 在构造时的函数 resolve() 和 reject() 来改变。
如果 x 处于等待状态,那么 promise 继续保持等待状态,等待解决过程函数 resolvePromise() 执行,否则应该用相同的值执行或拒绝 promise。我们无法从外部拒绝或执行一个 Promise 实例,只能通过调用构造函数传入的 resolve() 和 reject() 函数来实现。所以还需要把这两个函数作为参数传递到 resolvePromise 函数中。
在函数 resolvePromise() 内部加上情况 2 的判断,代码如下:
function resolvePromise(promise, x, resolve, reject) { ... if (x instanceof Promise) { if (x.state === FULFILLED) { resolve(x.value) } else if (x.state === REJECTED) { reject(x.reason) } else { x.then(function (y) { resolvePromise(promise, y, resolve, reject) }, reject) } } }
再来实现情况 3,将 x.then 取出然后执行,并将执行结果放入解决过程函数 resolvePromise() 中。 考虑到 x 可能只是一个 thenable 而非真正的 Promise,所以在调用 then() 函数的时候要设置一个变量 excuted 避免重复调用。同时记得在执行时添加异常捕获并及时拒绝当前 promise。
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { var executed; try { var then = x.then; if (typeof then === "function") { then.call(x, function (y) { if (executed) return; executed = true; return resolvePromise(promise, y, resolve, reject) }, function (e) { if (executed) return; executed = true; reject(e); }) } else { resolve(x); } } catch (e) { if (executed) return; executed = true; reject(e); } }
情况 4 就很简单了,直接把 x 作为值执行。
resolve(x);