异步发展流程 —— 手写一个符合 Promise/A+ 规范的 Promise
阅读原文
概述
Promise 是 js 异步编程的一种解决方案,避免了 “回调地狱” 给编程带来的麻烦,在 ES6 中成为了标准,这篇文章重点不是叙述 Promise 的基本用法,而是从零开始,手写一版符合 Promise/A+ 规范的 Promise,如果想了解更多 Promise 的基本用法,可以看 异步发展流程 —— Promise 的基本使用 这篇文章。
Promise 构造函数的实现
我们在使用 Promise 的时候其实是使用 new
关键字创建了一个 Promise 的实例,其实 Promise 是一个类,即构造函数,下面来实现 Promise 构造函数。
Promise/A+ 规范的内容比较多,详情查看 https://promisesaplus.com/,我们在实现 Promise 逻辑时会根据实现的部分介绍相关的 Promise/A+ 规范内容。
在 Promise/A+ 规范中规定:
- 构造函数的参数为一个名为
executor
的执行器,即函数,在创建实例时该函数内部逻辑为同步,即立即执行; executor
执行时的参数分别为resolve
和reject
,一个为成功时执行的函数,一个为失败时执行的函数;- 在
executor
执行时,一旦出现错误立即调用reject
函数,并设置错误信息给reason
属性; - 每个 Promise 实例有三个状态
pending
、fulfilled
和rejected
,默认状态为pending
; - 状态只能从
pending
到fulfilled
或从pending
到rejected
,且不可逆; - 执行
resolve
函数会使状态从pending
变化到fulfilled
并将参数存入实例的value
属性中; - 执行
reject
函数会使状态从pending
变化到rejected
并将错误信息存入实例的reason
属性中。
针对上面的 Promise/A+ 规范,Promise 构造函数代码实现如下:
// promise.js -- Promise 构造函数 function Promise(executor) { var self = this; self.status = "pending"; // 当前 Promise 实例的状态 self.value = undefined; // 当前 Promise 实例成功状态下的值 self.reason = undefined; // 当前 Promise 实例失败状态的错误信息 self.onFulfilledCallbacks = []; // 存储成功的回调函数的数组 self.onRejectedCallbacks = []; // 存储失败的回调函数的数组 // 成功的执行的函数 function resolve(value) { if (self.status === "pending") { self.status = "fulfilled"; self.value = value; // 每次调用 resolve 时,执行 onFulfilledCallbacks 内部存储的所有的函数(在实现 then 方法中详细说明) self.onFulfilledCallbacks.forEach(function(fn) { fn(); }); } } // 失败执行的函数 function reject(reason) { if (self.status === "pending") { self.status = "rejected"; self.reason = reason; // 每次调用 reject 时,执行 onRejectedCallbacks 内部存储的所有的函数(在实现 then 方法中详细说明) self.onRejectedCallbacks.forEach(function(fn) { fn(); }); } } // 调用执行器函数 try { executor(resolve, reject); } catch (e) { // 如果执行器执行时出现错误,直接调用失败的函数 reject(e); } } // 将自己的 Promise 导出 module.exports = Promise;
上面构造函数中的 resolve
和 reject
方法在执行的时候都进行了当前状态的判断,只有状态为 pending
时,才能执行判断内部逻辑,当两个函数有一个执行后,此时状态发生变化,再执行另一个函数时就不会通过判断条件,即不会执行判断内部的逻辑,从而实现了两个函数只有一个执行判断内部逻辑的效果,使用如下:
// verify-promise.js -- 验证 promise.js 的代码 // 引入自己的 Promise 模块 // 因为都验证代码都写在 verify-promise.js 文件中,后面就不再引入了 const Promise = require("./promise.js"); let p = new Promise((resolve, reject) => { // ...同步代码 resolve(); reject(); // 上面两个函数只有先执行的 resolve 生效 });
实例方法的实现
1、then 方法的实现
没有 Promise 之前在一个异步操作的回调函数中返回一个结果在输入给下一个异步操作,下一个异步操作结束后需要继续执行回调,就形成回调函数的嵌套,在 Promise 中,原来回调函数中的逻辑只需要调用当前 Promise 实例的 then
方法,并在 then
方法的回调中执行,改变了原本异步的书写方式。
在 then 方法中涉及到的 Promise/A+ 规范:
- Promise 实例的
then
方法中有两个参数,都为函数,第一个参数为成功的回调onFulfilled
,第二个参数为失败的回调onRejected
; - 当 Promise 内部执行
resolve
时,调用实例的then
方法执行成功的回调onFulfilled
,当 Promise 内部执行reject
或执行出错时,调用实例的then
方法执行错误的回调onRejected
; then
方法需要支持异步,即如果resovle
或reject
执行为异步时,then
方法的回调onFulfilled
或onRejected
需要在后面执行;- Promise 需要支持链式调用,Promise 实例调用
then
方法后需要返回一个新的 Promise 实例。如果then
的回调中有返回值且是一个 Promise 实例,则该 Promise 实例执行后成功或失败的结果传递给下一个 Promise 实例的then
方法onFulfilled
(成功的回调)或onRejected
(失败的回调)的参数,如果返回值不是 Promise 实例,直接将这个值传递给下一个 Promise 实例then
方法回调的参数,then
的回调如果没有返回值相当于返回undefined
; - Promise 实例链式调用
then
时,当任何一个then
执行出错,链式调用下一个then
时会执行错误的回调,错误的回调没有返回值相当于返回了undefined
,再次链式调用then
时会执行成功的回调; - Promise 实例的链式调用支持参数穿透,即当上一个
then
没有传递回调函数,或参数为null
时,需要后面调用的then
的回调函数来接收; executor
在 Promise 构造函数中执行时使用try...catch...
捕获异常,但是内部执行的代码有可能是异步的,所以需要在then
方法中使用try...catch...
再次捕获;- Promise 实例的
then
方法中的回调为micro-tasks
(微任务),回调内的代码应晚于同步代码执行,在浏览器内部调用微任务接口,我们这里模拟使用宏任务代替。
针对上面的 Promise/A+ 规范,then 方法代码实现如下:
// promise.js -- then 方法 Promise.prototype.then = function(onFulfilled, onRejected) { // 实现参数穿透 if(typeof onFulfilled !== "function") { onFulfilled = function (data) { return data; } } if(typeof onRejected !== "function") { onRejected = function (err) { throw err; } } // 返回新的 Promise,规范中规定这个 Promise 实例叫 promise2 var promise2 = new Promise(function (resolve, reject) { if (this.status === "fulfilled") { // 用宏任务替代模拟微任务,目的是使 `then` 的回调晚于同步代码执行 setTimeout(function () { try { // 捕获异步的异常 // onFulfilled 执行完返回值的处理,x 为成功回调的返回值 var x = onFulfilled(this.value); // 处理返回值单独封装一个方法 resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); } if (this.status === "rejected") { setTimeout(function () { try { // onRejected 执行完返回值的处理,x 为失败回调的返回值 var x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); } // 如果在 Promise 执行 resolve 或 renject 为异步 // 将 then 的执行程序存储在实例对应的 onFulfilledCallbacks 或 onRejectedCallbacks 中 if (this.status === "pending") { this.onFulfilledCallbacks.push(function() { setTimeout(function () { try { var x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); }); this.onRejectedCallbacks.push(function() { setTimeout(function () { try { var x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); }); } }); return promise2; };
在处理 then
回调的返回值时,其实就是在处理该返回值与 then
方法在执行后返回的新 Promise 实例(即 promise2)之间的关系,因为无论 Promise 的执行器在执行 resolve
还是 reject
是同步或是异步,都需要进行处理,所以我们单独封装一个函数 resolvePromise
来处理。
resolvePromise 函数有四个参数:
- promise2:
then
执行后返回的 Promise 实例; - x:
then
的回调返回的结果; - resolve:promise2 的
resolve
函数; - reject:promise2 的
reject
函数。
在 resolvePromise 函数中涉及到的 Promise/A+ 规范:
- 将每个 Promise 实例调用
then
后返回的新 Promise 实例称为promise2
,将then
回调返回的值称为x
; - 如果
promise2
和x
为同一个对象,由于x
要将执行成功或失败的结果传递promise2
的then
方法回调的参数,因为是同一个 Promise 实例,此时既不能成功也不能失败(自己不能等待自己完成),造成循环引用,这种情况下规定应该抛出一个类型错误来回绝; - 如果
x
是一个对象或者函数且不是null
,就去取x
的then
方法,如果x
是对象,防止x
是通过Object.defineProperty
添加then
属性,并添加get
和set
监听,如果在监听中抛出异常,需要被捕获到,x.then
是一个函数,就当作x
是一个 Promise 实例,直接执行x
的then
方法,执行成功就让promise2
成功,执行失败就让promise2
失败,如果x.then
不是函数,则说明x
为普通值,直接调用promise2
的resolve
方法将x
传入,不满足条件说明该返回值就是一个普通值,直接执行promise2
的resolve
并将x
作为参数传入; - 如果每次执行
x
的then
方法,回调中传入的参数还是一个 Promise 实例,循环往复,需要递归resolvePromise
进行解析; - 在递归的过程中存在内、外层同时调用了
resolve
和reject
的情况,应该声明一个标识变量called
做判断来避免这种情况。
针对上面的 Promise/A+ 规范,resolvePromise 函数代码实现如下:
// promise.js -- resolvePromise 方法 function resolvePromise(promise2, x, resolve, reject) { // 判断 x 和 promise2 是不是同一个函数 if (promise2 === x) { reject(new TypeError("循环引用")); } // x 是对象或者函数并且不是 null,如果不满足该条件说明 x 只是一个普通的值 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 标识变量,防止递归内外层 resolve 和 reject 同时调用 // 针对 Promise,x 为普通值的时候可以放行 var called; // 为了捕获 Object.defineProperty 创建的 then 属性时添加监听所抛出的异常 try { var then = x.then; if (typeof then === "function") { // then 为一个方法,就当作 x 为一个 promise // 执行 then,第一个参数为 this(即 x),第二个参数为成功的回调,第三个参数为失败的回调 then.call(x, function (y) { if (called) return; called = true; // 如果 y 是 Promise 就继续递归解析 resolvePromise(promise2, y, resolve, reject); }, function (err) { if (called) return; called = true; reject(err); }); } else { // x 是一个普通对象,直接成功即可 resolve(x); } } catch(e) { if (called) return; called = true; reject(e); } } else { resolve(x); } }
上面我们按照 Promise/A+ 规范实现了 Promise 的 then
方法,接下来针对上面的规范,用一些有针对行的案例来对 then
方法一一进行验证。
验证异步调用 resolve
或 reject
:
// 文件:verify-promise.js // 验证 promise.js 异步调用 resolve 或 reject let p = new Promise((resolve, reject) => { setTimeout(() => resolve(), 1000); }); p.then(() => console.log("执行了")); // 执行了
验证链式调用 then
返回 Promise 实例:
// 文件:verify-promise.js // 验证 promise.js then 回调返回 Promise 实例 let p1 = new Promise((resolve, reject) => resolve()); let p2 = new Promise((resolve, reject) => resolve("hello")); p1.then(() => p2).then(data => console.log(data)); // hello
验证链式调用 then
返回普通值:
// 文件:verify-promise.js // 验证 promise.js then 回调返回普通值 let p = new Promise((resolve, reject) => resolve()); p.then(() => "hello").then(data => console.log(data)); // hello
验证链式调用 then
中执行出错链式调用 then
执行错误的回调后,再次链式调用 then
:
// 文件:verify-promise.js // 验证 promise.js 链式调用 then 中执行出错链式调用 then 执行错误的回调后,再次链式调用 then let p = new Promise((resolve, reject) => resolve()); p.then(() => { throw new Error("error"); }).then(() => { console.log("success"); }, err => { console.log(err); }).then(() => { console.log("成功"); }, () => { console.log("失败"); }); // Error: error at p.then... // 成功
验证 then
的参数穿透:
// 文件:verify-promise.js // 验证 then 的参数穿透 let p1 = new Promise((resolve, reject) => resolve("ok")); let p2 = p1.then().then(data => { console.log(data); throw new Error("出错了"); }); p2.then().then(null, err => console.log(err)); // ok // 出错了
验证 then
方法是否晚于同步代码执行:
// 文件:verify-promise.js // 验证 then 方法是否晚于同步代码执行 let p = new Promise((resolve, reject) => { resolve(1); }); p.then(data => console.log(data)); console.log(2); // 2 // 1
验证循环引用:
// 文件:verify-promise.js // 验证 promise.js 循环引用 let p1 = new Promise((resolve, reject) => resolve()); // 让 p1 then 方法的回调返回自己 var p2 = p1.then(() => { return p2; }); p2.then(() => { console.log("成功"); }, err => { console.log(err); }); // TypeError: 循环引用 at resolvePromise...
验证 then
回调返回对象通过 Object.definePropertype
添加 then
属性并添加 get
监听,在触发监听时抛出异常:
// 文件:verify-promise.js // 验证 promise.js then 回调返回对象通过 Object.definePropertype 添加 then 和 get 监听,捕获异常 let obj = {}; Object.defineProperty(obj, "then", { get () { throw new Error(); } }); let p = new Promise((resolve, reject) => resolve()); p.then(() => { return obj; }).then(() => { console.log("成功"); }, () => { console.log("出错了"); }); // 出错了
验证每次执行 resolve
都传入 Promise 实例,需要将最终的执行结果传递给下一个 Promise 实例 then
的回调中:
// 文件:verify-promise.js // 验证 promise.js 每次执行 resolve 都传入 Promise 实例 let p = new Promise((resolve, reject) => resolve()); p.then(() => { return new Promise((resolve, reject) => { resolve(new Promise(resolve, reject) => { resolve(new Promise(resolve, reject) => { resolve(200); }); }); }); }).then(data => { console.log(data); }); // 200
2、catch 方法的实现
// promise.js -- catch 方法 Promise.prototype.catch = function (onRejected) { return this.then(null, onRejected); }
catch
方法可以理解为是 then
方法的一个简写,只是参数中少了成功的回调,所以利用 Promise/A+ 规范中参数穿透的特性,很容易就实现了 catch
方法,catch
方法的真相就是这么的简单。
验证 catch
方法:
// 文件:verify-promise.js // 验证 promise.js 的 catch 方法 let p = new Promise((resolve, reject) => reject("err")); p.then().catch(err => { console.log(err); }).then(() => { console.log("成功了"); }); // err // 成功了
静态方法的实现
1、Promise.resolve 方法的实现
Promise.resolve
方法传入一个参数,并返回一个新的 Promise 实例,这个参数作为新 Promise 实例 then
方法成功回调的参数,在调用时感觉直接成功了,其实是直接执行了返回 Promise 实例的 resolve
。
// promise.js -- Promise.resolve 方法 Promise.resolve = function (val) { return new Promise(function (resolve, reject) { resolve(val); }); }
验证 Promise.resolve
方法:
// 文件:verify-promise.js // 验证 promise.js 的 Promise.resolve 方法 Promise.resolve("成功了").then(data => console.log(data)); // 成功了
2、Promise.reject 方法的实现
Promise.reject
方法与 Promise.resolve
的实现思路相同,不同的是,直接调用了返回新 Promise 实例的 reject
。
// promise.js -- Promise.reject 方法 Promise.reject = function (reason) { return new Promise(function (resolve, reject) { reject(reason); }); }
验证 Promise.reject
方法:
// 文件:verify-promise.js // 验证 promise.js 的 Promise.reject 方法 Promise.reject("失败了").then(err => console.log(err)); // 失败了
3、Promise.all 方法的实现
Promise.all
方法可以实现多个 Promise 实例的并行,返回值为一个新的 Promise 实例,当所有结果都为成功时,返回一个数组,该数组存储的为每一个 Promise 实例的返回结果,这些 Promise 实例的返回顺序先后不确定,但是返回值的数组内存储的返回结果是按照数组中 Promise 实例最初顺序进行排列的,返回的数组作为返回 Promise 实例成功回调的参数,当其中一个失败,直接返回错误信息,并作为返回 Promise 实例失败回调的参数。
// promise.js -- Promise.all 方法 Promise.all = function (promises) { return new Promise(function (resolve, reject) { // 存储返回值 var result = []; // 代表存入的个数,因为 Promise 为异步,不知道哪个 Promise 先成功,不能用数组的长度来判断 var idx = 0; // 用来构建全部成功的返回值 function processData(index, data) { result[index] = data; // 将返回值存入数组 idx++; if (idx === promises.length) { resolve(result); } } for(var i = 0; i < promises.length; i++) { // 因为 Primise 为异步,保证 i 值是顺序传入 (function (i) { promises[i].then(function (data) { processData(i, data); }, reject); })(i); } }); }
验证 Promise.all
方法:
// 文件:verify-promise.js // 验证 promise.js 的 Promise.all 方法 let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)); Promise.all([p1, p2]).then(data => console.log(data)); // [1, 2] let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000)); Promise.all([p3, p4]).then(data => { console.log(data); }).catch(err => { console.log(err); }); // 2
4、Promise.race 方法的实现
Promise.race
方法与 Promise.all
类似,同样可以实现多个 Promise 实例的并行,同样返回值为一个新的 Promise 实例,参数同样为一个存储多个 Promise 实例的数组,区别是只要有一个 Promise 实例返回结果,无论成功或失败,则直接返回这个结果,并作为新 Promise 实例 then
方法中成功或失败的回调函数的参数。
// promise.js -- Promise.race 方法 Promise.race = function (promises) { return new Promise(function (resolve, reject) { for(var i = 0; i < promises.length; i++) { promises[i].then(resolve, reject); } }); }
验证 Promise.race
方法:
// 文件:verify-promise.js // 验证 promise.js 的 Promise.race 方法 let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)); Promise.race([p1, p2]).then(data => console.log(data)); // 2 let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000)); Promise.all([p3, p4]).then(data => { console.log(data); }).catch(err => { console.log(err); }); // 2
使用 promises-aplus-test 测试 Promise 是否符合 Promise/A+ 规范
promises-aplus-test
是专门用来验证 Promise 代码是否符合 Promise/A+ 规范的包,需要通过 npm
下载。
npm install promises-aplus-test -g
测试方法:
- 在
promise.js
中写入测试代码; - 在命令行中输入命令
promises-aplus-test
+fileName
。
测试代码:
// promise.js -- 测试方法 Promise.derfer // Promise 语法糖 // 好处:解决 Promise 嵌套问题 // 坏处:错误处理不方便 Promise.derfer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; }
输入命令:
promises-aplus-test promise.js
执行上面命令后,会根据 Promise/A+ 规范一条一条进行极端的验证,当验证通过后会在窗口中这一条对应的执行项前打勾,验证不通过打叉,直到所有的规范都验证完毕。