前言
手写 Promise 是面试的时候大家都逃避的送命题,在学些了解后发现通过实现源码更能将新一代的异步方案理解的通透,知其然知其所以然的运用。
如果直接将源码贴到此处势必不能有更大的收获,下面就按实现版本来看做简要分析。
回顾 Promise
Promise 是 CommonJS 提出来的这一种规范,有多个版本,在 ES6 当中已经纳入规范,原生支持 Promise 对象,非 ES6 环境可以用类似 Bluebird、Q 这类库来支持。
Promise 可以将回调变成链式调用写法,流程更加清晰,代码更加优雅,还可以批量处理异步任务。
简单归纳下 Promise:三个状态、两个过程、一个方法,快速记忆方法:3-2-1
三个状态:pending、fulfilled、rejected
两个过程:
- pending → fulfilled(resolve)
- pending → rejected(reject)
一个方法:then
当然还有其他概念,如 catch、 Promise.all/race/allSettled。
基础版
基础测试用例
// 1. 链式调用
var p1 = new Promise(function (resolve, reject) {console.log("init Promise");if (Math.random() > 0.5) {resolve("大");} else {reject("小");}
});
p1.then((data) => console.log("success", data),(reason) => console.log("error", reason)
).then(() => console.log("success 2"),() => console.log("error 2")
);
// 2. 异步延时
var sleep = (time, data) =>new Promise(function (resolve, reject) {setTimeout(resolve, time, data);});
sleep(3000, "时间到!").then((val) => {console.log(val);
});
// 3. 状态变更后不可变
const p2 = new Promise(function (resolve, reject) {resolve("失败了!");reject("还会成功吗!");
});
p2.then((data) => console.log(data),(reason) => console.log(reason)
);
// Promise 打印日志:
// init Promise
// success 大 / error 小
// 失败了!
// success 2 /error 2
// 时间到!(延时 3 s)
Promise 的基本特征
1.new promise 时, 需要传入一个立即执行函数 fn,fn 接受两个参数,分别是 resolve 和 reject;
2.promise 有三个状态:pending,fulfilled,or rejected, 默认状态是 pending,只能从 pending 到 rejected, 或者从 pending 到 fulfilled,状态一旦确认,就不会再改变;
3.promise 必须有一个 then 方法,then 接收两个参数,分别是 promise 成功的回调 fulfilledFn, 和 promise 失败的回调 rejectedFn;
4.promise then 支持链式调用。
手写基础版
class Promise {constructor(executor) {this.status = "pending";this.handleFulfilled = []; // 存储成功后的回调this.handleRejection = []; // 存储失败后的回调// ! resolve 形参的实际参数在这儿const resolve = (data) => {// 状态变更只有一次if (this.status !== "pending") {return;}this.status = "fulfilled";// ! 等一会,否则 handleFulfilled 为空setTimeout(() => {this.handleFulfilled.forEach((fn) => fn(data));}, 0);};const reject = (reason) => {if (this.status !== "pending") {return;}this.status = "rejected";setTimeout(() => {this.handleRejection.forEach((fn) => fn(reason));}, 0);};try {executor(resolve, reject);} catch (e) {// 遇到错误时,捕获错误,执行 reject 函数reject(e);}}then(fulfilledFn, rejectedFn) {this.handleFulfilled.push(fulfilledFn);this.handleRejection.push(rejectedFn);return this;}
}
测试用例:
// 简易版 Promise 打印日志:
// init Promise
// success 大 / error 小
// success 2 /error 2 (x 未通过)
// 失败了!
// 时间到!(延时 3 s)
存在的问题:
1.微任务宏任务队列打印顺序
2.then 链式调用不是通过返回 this,而是返回一个新的 promise
3.链式调用支持参数缺省
4.等等…
认识 Promise /A+ 规范
手写 Promise,需要遵守怎样的规则,业界所有 Promise 的类库都遵循 Promise/A+ 规范。译文
改进版
先按 Promise 的基本规范对上面的基础版进行改进。
1.new promise 时, 需要传入一个立即执行函数executor
,executor
接受两个参数,分别是 resolve 和 reject;
2.promise 有三个状态:pending
,fulfilled
,or rejected
, 默认状态是 pending
,只能从 pending
到 rejected
, 或者从 pending
到 fulfilled
,状态一旦确认,就不会再改变;「规范 Promise/A+ 2.1」
3.promise 有一个value
保存成功状态的值,可以是undefined/thenable/promise
;「规范 Promise/A+ 1.3」
4.promise 有一个reason
保存失败状态的值;「规范 Promise/A+ 1.5」
5.promise 必须有一个 then 方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled, 和 promise 失败的回调 onRejected;「规范 Promise/A+ 2.2」
6.如果调用 then 时,promise 已经成功,则执行onFulfilled
,参数是 promise 的value
;
7.如果调用 then 时,promise 已经失败,那么执行 onRejected
, 参数是 promise 的 reason
;
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
class Promise {constructor(executor) {this.status = PENDING; // 默认状态为 PENGINGthis.value = undefined; // 存放成功状态得值this.reason = undefined; // 存放失败状态得值this.handleFulfilled = []; // 存储成功后的回调this.handleRejection = []; // 存储失败后的回调// ! resolve 形参的实际参数在这儿const resolve = (data) => {// 状态变更只有一次if (this.status === PENDING) {this.status = FULFILLED;this.value = data;this.handleFulfilled.forEach((fn) => fn(data));}};const reject = (reason) => {if (this.status === PENDING) {this.status = REJECTED;this.reason = reason;this.handleRejection.forEach((fn) => fn(reason));}};try {executor(resolve, reject);} catch (e) {// 遇到错误时,捕获错误,执行 reject 函数reject(e);}}then(onFulfilled, onRejected) {if (this.status === FULFILLED) {onFulfilled(this.value);}if (this.status === REJECTED) {onRejected(this.reason);}if (this.status === PENDING) {this.handleFulfilled.push(() => onFulfilled(this.value));this.handleRejection.push(() => onRejected(this.reason));}return this;}
}
Promise A+规范版
规范思路梳理
如果调用 then 时,promise 已经成功,则执行 onFulfilled,并将 promise 的值作为参数传递进去。 如果 promise 已经失败,那么执行 onRejected, 并将 promise 失败的原因作为参数传递进去。如果 promise 的状态是 pending,需要将 onFulfilled 和 onRejected 函数存放起来,等待状态确定后,再依次将对应的函数执行(发布订阅)
1.then 的参数 onFulfilled
和 onRejected
可以缺省,如果 onFulfilled
或者 onRejected
不是函数,将其忽略,且依旧可以在下面的 then
中获取到之前返回的值;「规范 Promise/A+ 2.2.1、2.2.1.1、2.2.1.2」
2.promise 可以 then 多次,promise 的 then 方法返回一个新的 promise 「规范 Promise/A+ 2.2.7」
3.如果 then 返回值 x 是一个普通值,那么就会把这个结果作为参数,传递给下一个 then 的成功的回调(onFulfilled)
4.如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个 then 的失败的回调(onRejected) 规范 Promise/A+ 2.2.7.2」
5.如果 then 的返回值 x 是一个 promise,那么会等这个 promise 执行完,promise 如果成功,就走下一个 then 的成功;如果失败,就走下一个 then 的失败;如果抛出异常,就走下一个 then 的失败;「规范 Promise/A+ 2.2.7.3、2.2.7.4」
6.如果 then 的返回值 x 和 promise 是同一个引用对象,造成循环引用,则抛出异常,把异常传递给下一个 then 的失败的回调中;「规范 Promise/A+ 2.3.1」
7.如果 then 的返回值 x 是一个 promise,且 x 同时调用 resolve 函数和 reject 函数,则第一次调用优先,其他所有调用被忽略;「规范 Promise/A+ 2.3.3.3.3」
源码实现
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
// 将 onFufilled 的返回值进行判断取值处理,把最后获得的普通值放入最外面那层的 Promise 的 resolve 函数中
const resolvePromise = (promise2, x, resolve, reject) => {// 自己等待自己完成是错误的实现,用一个循环引用的类型错误,结束掉 promisePromise/A+ 2.3.1if (promise2 === x) {return reject(new TypeError("Chaining cycle detected for promise #<Promise>"));}// 只能调用一次,为了判断resolve过的就不用再reject了,(比如有reject和resolve的时候)Promise/A+ 2.3.3.3.3let called;// 如果 x 不是null,是对象或者方法if ((typeof x === "object" && x != null) || typeof x === "function") {try {// 这个首先存储对 x.then 的引用,然后测试该引用let then = x.then;if (typeof then === "function") {// 那我们就认为他是promise,call他,因为then方法中的this来自自己的promise对象// 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.definePropertyPromise/A+ 2.3.3.3// 第一个参数是将x这个promise方法作为this指向,后两个参数分别为成功失败回调then.call(x,(y) => {// 根据 promise 的状态决定是成功还是失败if (called) return;called = true;// 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1resolvePromise(promise2, y, resolve, reject);},(r) => {// 只要失败就失败 Promise/A+ 2.3.3.3.2if (called) return;called = true;reject(r);});} else {// 如果 x.then 是个普通值就直接返回 resolve 作为结果Promise/A+ 2.3.3.4resolve(x);}} catch (e) {// Promise/A+ 2.3.3.2if (called) return;called = true;reject(e);}} else {// 如果 x 是个普通值就直接返回 resolve 作为结果Promise/A+ 2.3.4resolve(x);}
};
class Promise {constructor(executor) {this.status = PENDING;this.value = undefined;this.reason = undefined;this.onResolvedCallbacks = [];this.onRejectedCallbacks = [];let resolve = (value) => {if (this.status === PENDING) {this.status = FULFILLED;this.value = value;this.onResolvedCallbacks.forEach((fn) => fn());}};let reject = (reason) => {if (this.status === PENDING) {this.status = REJECTED;this.reason = reason;this.onRejectedCallbacks.forEach((fn) => fn());}};try {executor(resolve, reject);} catch (error) {reject(error);}}then(onFulfilled, onRejected) {//解决 onFufilled,onRejected 没有传值的问题//Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;//因为错误的值要让后面访问到,所以这里也要抛出个错误,不然会在之后 then 的 resolve 中捕获onRejected =typeof onRejected === "function"? onRejected: (err) => {throw err;};// 每次调用 then 都返回一个新的 promisePromise/A+ 2.2.7let promise2 = new Promise((resolve, reject) => {if (this.status === FULFILLED) {//Promise/A+ 2.2.2//Promise/A+ 2.2.4 --- setTimeout 宏任务模拟异步setTimeout(() => {try {//Promise/A+ 2.2.7.1// 因为有的时候需要判断then中的方法是否返回一个promise对象,所以需要判断// 如果返回值为promise对象,则需要取出结果当作promise2的resolve结果// 如果不是,直接作为promise2的resolve结果let x = onFulfilled(this.value);// x可能是一个proimise// 抽离出一个公共方法来判断他们是否为promise对象resolvePromise(promise2, x, resolve, reject);} catch (e) {//Promise/A+ 2.2.7.2reject(e);}}, 0);}if (this.status === REJECTED) {//Promise/A+ 2.2.3setTimeout(() => {try {let x = onRejected(this.reason);resolvePromise(promise2, x, resolve, reject);} catch (e) {reject(e);}}, 0);}if (this.status === PENDING) {this.onResolvedCallbacks.push(() => {setTimeout(() => {try {let x = onFulfilled(this.value);resolvePromise(promise2, x, resolve, reject);} catch (e) {reject(e);}}, 0);});this.onRejectedCallbacks.push(() => {setTimeout(() => {try {let x = onRejected(this.reason);resolvePromise(promise2, x, resolve, reject);} catch (e) {reject(e);}}, 0);});}});return promise2;}
}
测试一下,打印日志也对了:
// Promise 打印日志:
// init Promise
// error 小
// 失败了!
// success 2
// 时间到!(延时 3 s)
再试试 then 的链式调用和值的穿透:
const promise = new Promise((resolve, reject) => {reject("失败");
}).then().then().then((data) => {console.log(data);},(err) => {console.log("[Error]:", err); //log: [Error]: 失败});
完善 Promise API
虽然上述的 promise 源码已经符合 Promise/A+ 的规范,但是原生的 Promise 还提供了一些其他方法,如:
Promise.prototype.catch()
Promise.prototype.finally()
Promise.resolve(value)
—— 使用给定 value 创建一个 resolved 的 promise。Promise.reject(error)
—— 使用给定 error 创建一个 rejected 的 promise。Promise.all(promises)
—— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。Promise.race(promises)
—— 等待第一个 settle 的 promise,并将其 result/error 作为结果返回。Promise.any(promises)
(ES2021 新增方法)—— 等待第一个 fulfilled 的 promise,并将其结果作为结果返回。如果所有 promise 都 rejected,Promise.any 则会抛出 AggregateError 错误类型的 error 实例。Promise.allSettled(promises)
(ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:* status: “fulfilled” 或 “rejected”* value(如果 fulfilled)或 reason(如果 rejected)。
下面具体说一下每个方法的实现:
Promise.prototype.catch
先从错误捕获开始,catch 用来捕获 promise 的异常,就相当于一个没有成功的 then。
Promise.prototype.catch = function (errCallback) {return this.then(undefined, errCallback);
};
测试例子:
// 抛出一个错误,大多数时候将调用catch方法
var p1 = new Promise(function (resolve, reject) {throw "Uh-oh!";
});
p1.catch(function (e) {console.log(e); // "Uh-oh!"
});
// 在异步函数中抛出的错误不会被catch捕获到
var p2 = new Promise(function (resolve, reject) {setTimeout(function () {throw "Uncaught Exception!";}, 1000);
});
p2.catch(function (e) {console.log(e); // 不会执行
});
Promise.prototype.finally()
finally 方法,在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。 这避免了同样的语句需要在 then()和 catch()中各写一次的情况。
Promise.prototype.finally = function (callback) {return this.then((value) => {return Promise.resolve(callback(value)).then(() => value);},(reason) => {return Promise.resolve(callback(reason)).then(() => {throw reason;});});
};
测试用例:
const promise1 = Promise.resolve(123);
promise1.then((value) => {return value;}).finally((res) => {console.log("finally", res); // finally 123});
Promise.resolve(456).finally(() => {return new Promise((resolve, reject) => {setTimeout(() => {resolve(123);}, 3000);});}).then((data) => {console.log(data, "success"); // 3秒后,456 'success'}).catch((err) => {console.log(err, "error");});
Promise.resolve
默认产生一个成功的 promise。Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是 thenable(即带有"then" 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终状态;否则返回的 promise 将以此值完成。此函数将类 promise 对象的多层嵌套展平。
static resolve(data){return new Promise((resolve,reject)=>{resolve(data);})
}
如果参数是 promise 会等待这个 promise 解析完毕,在向下执行,所以这里需要在 constructor 方法中做一个小小的处理:
let reject = (reason) => {if (this.status === PENDING) {this.status = REJECTED;this.reason = reason;this.onRejectedCallbacks.forEach((fn) => fn());}
};
let resolve = (value) => {// 如果 value 是一个promise,那我们的库中应该也要实现一个递归解析if (value instanceof Promise) {// 递归解析return value.then(resolve, reject);}if (this.status === PENDING) {this.status = FULFILLED;this.value = value;this.onResolvedCallbacks.forEach((fn) => fn());}
};
测试例子:
const promise1 = Promise.resolve(123);
promise1.then((value) => {console.log(value);// expected output: 123
});
Promise.resolve(new Promise((resolve, reject) => {setTimeout(() => {if (Math.random() > 0.5) {resolve("ok");} else {reject("err");}}, 3000);})
).then((data) => {console.log(data, "success"); // ok success}).catch((err) => {console.log(err, "error"); // err error});
Promise.reject()
默认产生一个失败的 promise,Promise.reject 是直接将值变成错误结果。
static reject(reason){return new Promise((resolve,reject)=>{reject(reason);})
}
Promise.all()
promise.all 是解决并发问题的,多个异步并发获取最终的结果(如果有一个失败则失败)。
Promise.all = function (values) {if (!Array.isArray(values)) {const type = typeof values;return new TypeError(`TypeError: ${type} ${values} is not iterable`);}return new Promise((resolve, reject) => {let resultArr = [];let orderIndex = 0;const processResultByKey = (value, index) => {resultArr[index] = value;if (++orderIndex === values.length) {resolve(resultArr);}};for (let i = 0; i < values.length; i++) {let value = values[i];if (value && typeof value.then === "function") {value.then((value) => {processResultByKey(value, i);}, reject);} else {processResultByKey(value, i);}}});
};
测试一下:
Promise.all([new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2new Promise((resolve) => setTimeout(() => resolve(3), 1000)), // 3
]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素
Promise.all([new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Whoops!")), 2000)),new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).catch(alert); // Error: Whoops!
Promise.race()
Promise.race 用来处理多个请求,采用最快的(谁先完成用谁的)。
Promise.race = function (promises) {return new Promise((resolve, reject) => {// 一起执行就是for循环for (let i = 0; i < promises.length; i++) {let val = promises[i];if (val && typeof val.then === "function") {val.then(resolve, reject);} else {// 普通值resolve(val);}}});
};
这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。
例如,这里的结果将是 1:
Promise.race([new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Whoops!")), 2000)),new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(alert); // 1
Promise.any()
与 Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回,它不会等待其他的 promise 全部完成。如果给出的 promise 都 rejected,那么则返回 rejected 的 promise 和 AggregateError 错误类型的 error 实例—— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。
例如,这里的结果将是 1:
Promise.any([new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Whoops!")), 1000)),new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(alert); // 1
这里的第一个 promise 是最快的,但 rejected 了,所以第二个 promise 则成为了结果。在第一个 fulfilled 的 promise “赢得比赛”后,所有进一步的结果都将被忽略。
这是一个所有 promise 都失败的例子:
Promise.any([new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Ouch!")), 1000)),new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Error!")), 2000)),
]).catch((error) => {console.log(error.constructor.name); // AggregateErrorconsole.log(error.errors[0]); // Error: Ouch!console.log(error.errors[1]); // Error: Error!
});
正如你所看到的,我们在 AggregateError 错误类型的 error 实例的 errors 属性中可以访问到失败的 promise 的 error 对象。
实现该方法,可以通过上面的两个例子:
Promise.any 只要传入的 promise 有一个是 fullfilled 则立即 resolve 出去,否则将所有 reject 结果收集起来并返回 AggregateError
Promise.any = function (promises) {return new Promise((resolve, reject) => {promises = Array.isArray(promises) ? promises : [];let len = promises.length;// 用于收集所有 rejectlet errs = [];// 如果传入的是一个空数组,那么就直接返回 AggregateErrorif (len === 0)return reject(new AggregateError("All promises were rejected"));promises.forEach((promise) => {promise.then((value) => {resolve(value);},(err) => {len--;errs.push(err);if (len === 0) {reject(new AggregateError(errs));}});});});
};
Promise.allSettled()
如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组具有:
- {status:“fulfilled”, value:result} 对于成功的响应,
- {status:“rejected”, reason:error} 对于 error。
Promise.allSettled = function (promises) {const rejectHandler = (reason) => ({ status: "rejected", reason });const resolveHandler = (value) => ({ status: "fulfilled", value });const convertedPromises = promises.map((p) =>Promise.resolve(p).then(resolveHandler, rejectHandler));return Promise.all(convertedPromises);
};
测试一下:
Promise.allSettled([new Promise((resolve) => setTimeout(() => resolve(1), 3000)), // 1new Promise((resolve) => setTimeout(() => resolve(2), 2000)), // 2new Promise((resolve) => setTimeout(() => resolve(3), 1000)), // 3
]).then(console.log); // [{status: 'fulfilled', value: 1}, {status: 'fulfilled', value: 2}, {status: 'fulfilled', value: 3}]
Promise.allSettled([new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),new Promise((resolve, reject) =>setTimeout(() => reject(new Error("Whoops!")), 2000)),new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)),
]).then(console.log); // [{status: 'fulfilled', value: 1}, {status: 'rejected', value: "Error: Whoops!"}, {status: 'fulfilled', value: 3}]测试一下:
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享