我的测试环境: chorme 85.0.4183.83
写作动机:
最近在手写Promise时,在实现过程中发现Promise核心思路并不复杂, 只不过细节众多, 很容易深陷逻辑分支漩涡。因此想和大家分享以下我手写Promise的思路。同时希望各位看官老爷帮我测试一下是否和原生Promise表现一致,先给老爷们跪下了~~QAQ
基础版本
根据Promise用法, 先实现一个基础版本的Promise,具有以下功能:
1. Promise实例具有state属性表示自身状态, 实例具有三种状态: pending、fulfilled、rejected, 初始值为pending
2. Promise实例具有result属性表示结果。初始值为undefined。
3. Promise实例的状态(state)和结果(result)值只能通过resolve,reject修改, 无法通过修改实例属性修改其值,其中state从pending变为settleed状态(fulfilled、rejected)之后, 就不可变了
4. 实例化Promise对象时, 构造函数执行”executor”函数参数, 并传给”executor”函数两个函数:resolve和reject,分别能够将Promise实例的状态(state)变为”fulfilled”和”rejected”, 同时将实例的结果(result)值设为resolve或reject函数接受的参数值。
5. 如果执行executor报错将会返回一个状态为rejected的Promise, 结果为报错信息
const STATE_PENDING = "pending";const STATE_FULFILLED = "fulfilled";const STATE_REJECTED = "rejected";function Promise(executor){if (typeof executor !== "function") {throw new Error("Promise resolver undefined is not a function");}let _self = thislet _state = STATE_PENDINGlet _result = undefinedObject.defineProperties(_self, {state: {get: () => _state,set: () => _state,enumerable: true},result: {get: () => _result,set: () => _result,enumerable: true}});function resolve(newResult) {if(_state !== STATE_PENDING) {return}_self.state = _state = STATE_FULFILLED_self.result = _result = newResult}function reject(newResult) {if(_state !== STATE_PENDING) {return}_self.state = _state = STATE_REJECTED_self.result = _result = newResult}try {executor(resolve, reject)} catch(e) {reject(e)}}
测试结果:
new Promise() // Uncaught Error: Promise resolver undefined is not a functionconsole.log(JSON.stringify(new Promise(() => {})))// {"state":"pending"}console.log(JSON.stringify(new Promise((resolve) => resolve())))// {"state":"fulfilled"}console.log(JSON.stringify(new Promise((resolve, reject) => reject())))// {"state":"rejected"}console.log(new Promise(() => {throw new Error("executor error!")}))// {// result: Error: executor error! at <anonymous>:2:8 at new Promise (<anonymous>:44:4) at <anonymous>:1:9// state: "rejected"// }let promise = new Promise(() => {})console.log(JSON.stringify(promise)) // {"state":"pending"}promise.state = "rejected"promise.result = "error!"console.log(JSON.stringify(promise)) // {"state":"pending"}let promise = new Promise((resolve, reject) => {resolve();reject();})console.log(promise) // {"state":"fulfiiled"}
到目前为止, 一个基础版的Promise的构造函数算是完成了。我们已经能实例化一个Promise,它具有state、result属性,其值只能通过resolve,reject更改, 不能被外部修改。
现在我们开始给Promise添加更多功能。
回调函数
Promise实例能够调用then、catch函数、finally。这三个函数将会给实例注册回调函数。同时返回一个新的Promise实例。
当Promise实例的状态发生改变后, 将异步执行回调函数。如果状态变成fulfilled执行onFulfilled函数, rejected执行onRejected函数。执行异步函数时, 会将Promise实例的result作为参数传入异步函数。
其中then能够注册onFulfilled、onRejecte方法,catch注册onRejected方法。finally和then相似(后面会列出差异)
简单理解:then、finally、catch其实就是给实例注册回调函数,而resolve、reject除了改变自身的state和result还需要异步执行回调函数。
现在我们重构Promise,使其能够使用then、finally、catch注册并异步执行回调函数。
// 修改resolve、rejectfunction resolve(newResult) {if (_state !== STATE_PENDING) {return;}_self.state = _state = STATE_FULFILLED;_self.result = _result = newResult;setTimeout(handleCallback);}function reject(newResult) {if (_state !== STATE_PENDING) {return;}_self.state = _state = STATE_REJECTED;_self.result = _result = newResult;setTimeout(handleCallback);}function handleCallback() {let callback;switch (_state) {case STATE_FULFILLED:callback = _self.callback && _self.callback.onFulfilled;break;case STATE_REJECTED:callback = _self.callback && _self.callback.onRejected;break;default:}callback(_result);}// 新增Promise.prototype.then = function (onFulfilled, onRejected) {this.callback = {onFulfilled,onRejected}};Promise.prototype.catch = function (onRejected) {return this.then(null, onRejected);};Promise.prototype.finally = function (onFinally) {return this.then(onFinally, onFinally);};
链式调用
Promise最大的特点是链式回调,通过链式调用能够将依赖性比较强的异步任务按序执行。通过传统方式回调深渊也能够实现, 但是显而易见, 在写法上Promise更加优雅、简洁易懂。
但是Promise链式回调同样是在手写过程中最棘手的部分, 因为其中有许多细节。
接下来我说一下特征和难点
特征:
1. 不使用finally的情况下,Promise回调函数链式执行,如果执行回调函数成功,那么执行下一个Promise的onFulfilled回调函数,传入当前回调函数返回值。如果执行回调函数失败,那么执行下一个Promise的onRejected回调函数,传入当前回调函数返回值。
2. 使用finally的情况下, 回调函数不会接受上一个回调函数的结果,并且执行下一个Promise的哪个回调函数由上一个Promise的状态决定, 传给下一个回调函数的参数也是上一个Promise的result。
3. 如果返回值是Promise, 如果Promise没有异常,无论是否为finally注册的回调函数,则回调函数将会等到返回的Promise状态settled之后触发。如果返回 实例化Promise过程报错, 那么将不会触发下一个Promise的onReject。
总之就是一句话:finally比较特殊, 需要单独做大量的处理。
现在有以下问题:then、catch、finally给当前Promise实例注册回调函数,返回了一个新的Promise,而回调函数会链式执行。那么当前Promise实例在执行完回调函数之后,是如何执行新的Promise回调函数的?
由于Promise的state和result只能通过resolve和reject函数改变实例state和result的值。
然后,在设计代码的时候,我草率地决定采取单链实现Promise的异步回调。通过then、catch、finally注册回调函数的同时,将新的Promise的resolve,reject函数绑定在当前Promise实例上, 这样就能够做到Promise实例在执行回调函数时,通过执行下一个Promise的resolve和reject函数,控制下一个Promise的state和result。
由于resolve和reject函数会异步执行回调函数,这样就可以实现Promise回调函数会一直沿着链执行回调函数
这个时候我对Promise链式执行过程的有了那么一内内的理解:
- 实例化了一个Promise, 在某个时刻会触发resolve或者reject函数
- then给Promise实例注册了回调函数(onFulfilled、onRejected),同时返回一个实例化的新的Promise。在使用构造函数实例化新的Promise过程中, 将新的Promise的内部方法resolve和reject的引用保存在Promise实例上, 使得Promise实例能够控制所有新产生的Promise的state和result
- catch原理和then一样, 只不过只注册onRejected函数
- finally原理同then一样, 只不过onFulfilled和onRejected都是同一个回调函数, 同时在Promise实例上标记finally帮助在执行回调函数时做一些处理
- 当resolve,reject触发。同步地更改Promise实例state和result,异步执行内部回调方法handleChainWork。
- handleChainWork主要做两件事:同步执行回调函数,异步改变下一个Promise的stae和result(通过resolve、reject)。当下一个Promise的resolve和reject触发, 又开始做第5点。
然后我实现了Promise能够链式调用的代码。主要处理逻辑在handleChainWork中,下面是实现Promise链式调用的代码:
const STATE_PENDING = "pending";const STATE_FULFILLED = "fulfilled";const STATE_REJECTED = "rejected";const defaultCallback = (result) => result;const handleUncaughtRejection = (err) => console.error(err);const curry = (fn) => {const args = [];return function reducer(param) {args.push(param);if (args.length < fn.length) {return reducer;}fn(...args);};};const handleChainWork = function (subscriber, promiseCallback = {}) {const state = this.state;const originResult = this.result;const isFinallyCb = this.isFinallyCb;let callbackResult;let deliveredResult;let callback;let wakeupChild;let hasCallbakck;switch (state) {case STATE_FULFILLED:hasCallbakck = !!promiseCallback.onFulfilled;callback = promiseCallback.onFulfilled || defaultCallback;wakeupChild = subscriber.resolve;break;case STATE_REJECTED:hasCallbakck = !!promiseCallback.onRejected;callback =promiseCallback.onRejected ||(this.subscribers.length ? defaultCallback : handleUncaughtRejection);wakeupChild =((isFinallyCb || !hasCallbakck) && subscriber.reject) ||subscriber.resolve;break;default:}// excute callbacktry {if (isFinallyCb) {callbackResult = callback();deliveredResult = originResult;} else {deliveredResult = callbackResult = callback(originResult);}} catch (e) {if (isFinallyCb) {setTimeout(() => wakeupChild(deliveredResult));throw new Error(e);} else {subscriber.reject(e);}}// case callback return a Promiseif (callbackResult instanceof Promise || deliveredResult instanceof Promise) {let promiseCallback = [subscriber.resolve, subscriber.reject];if (callbackResult instanceof Promise) {callbackResult.then(...promiseCallback);} else {deliveredResult.then(...promiseCallback);}return;}setTimeout(() => wakeupChild(deliveredResult));};function Promise(executor) {if (typeof executor !== "function") {throw new Error("Promise resolver undefined is not a function");}// in case "this" point to window in setTimeoutconst _self = this;// state & result is immutable becauseof closure// but they also can be gotten by this.XXXlet _state = STATE_PENDING;let _result = undefined;Object.defineProperties(_self, {state: {get: () => _state,set: () => _state,enumerable: true},result: {get: () => _result,set: () => _result,enumerable: true}});_self.subscribers = [];_self.callbacks = [];function handleStateChange(newState, result) {if (_state !== STATE_PENDING) {return;}_self.result = _result = result;_self.state = _state = newState;setTimeout(() => {_self.subscribers.forEach((subscriber, index) => {handleChainWork.bind(_self)(subscriber, _self.callbacks[index]);});});}const resolve = curry(handleStateChange)(STATE_FULFILLED);const reject = curry(handleStateChange)(STATE_REJECTED);try {executor(resolve, reject);} catch (error) {reject(error);}}Promise.prototype.then = function (onFulfilled, onRejected) {return new Promise((next_res, next_rej) => {this.subscribers.push({ resolve: next_res, reject: next_rej });this.callbacks.push({ onFulfilled, onRejected });});};Promise.prototype.catch = function (onRejected) {return this.then(null, onRejected);};Promise.prototype.finally = function (onFinally) {this.isFinallyCb = true;return this.then(onFinally, onFinally);};export default Promise;
这个时候我可牛逼坏了,Promise我也能手写出来了, 可以发个博客装逼了。
但是我却发现大佬们写的Promise全是清一色的队列,我还隐隐约约看到了秘籍(可能是忍者秘籍上的知识点吧),既然是队列, 该不会Promise还可以支持多个回调函数吧?
我这么一试,还真是:
let promise_1 = new Promise((res, rej) =>Math.random() * 2 > 1 ? res("行动开始") : rej("有内鬼, 交易终止"));promise_1.then(() => console.log("代少知道了"));promise_1.then(() => console.log("小王知道了"));promise_1.catch((error) => console.log(error));// 代少知道了// 小王知道了

所以我们需要重新调整思路, 不过大致的思路还是一样的。只不过从单链结构转换成了树状结构。也就是说:
- 当使用then、catch、finally创建了一个新的Promise:pChild时,需要将注册的回调函数放到当前Promise实例的callbackArray中去, pChild的resolve和reject同样放到当前Promise实例的childStateControllerArray中去。
- 当Promise状态改变时,异步执行函数,该函数遍历callbackArray数组, 执行handleChainWork(核心工作基本没有发生太多的变化)。
这是实现了Promise支持多个回调函数后的代码:
const STATE_PENDING = "pending";const STATE_FULFILLED = "fulfilled";const STATE_REJECTED = "rejected";const defaultCallback = (result) => result;const handleUncaughtRejection = (err) => console.error(err);const curry = (fn) => {const args = [];return function reducer(param) {args.push(param);if (args.length < fn.length) {return reducer;}fn(...args);};};const handleChildWork = function (child, promiseCallback = {}) {const state = this.state;const originResult = this.result;const isFinallyCb = this.isFinallyCb;let callbackResult;let deliveredResult;let readyCallback;let wakeupChild;let hasCallbakck;switch (state) {case STATE_FULFILLED:hasCallbakck = !!promiseCallback.onFulfilled;readyCallback = promiseCallback.onFulfilled || defaultCallback;wakeupChild = child.resolve;break;case STATE_REJECTED:hasCallbakck = !!promiseCallback.onRejected;readyCallback = promiseCallback.onRejected || defaultCallback;wakeupChild =((isFinallyCb || !hasCallbakck) && child.reject) || child.resolve;break;default:}// excute callbacktry {if (isFinallyCb) {callbackResult = readyCallback();deliveredResult = originResult;} else {deliveredResult = callbackResult = readyCallback(originResult);}} catch (e) {if (isFinallyCb) {setTimeout(wakeupChild, 0, deliveredResult);throw new Error(e);} else {child.reject(e);}}// case callback return a Promiseif (callbackResult instanceof Promise || deliveredResult instanceof Promise) {let promiseCallback = [child.resolve, child.reject];if (callbackResult instanceof Promise) {callbackResult.then(...promiseCallback);} else {deliveredResult.then(...promiseCallback);}return;}setTimeout(wakeupChild, 0, deliveredResult);};function Promise(executor) {if (typeof executor !== "function") {throw new Error("Promise resolver undefined is not a function");}// in case "this" point to window in setTimeoutconst _self = this;// state & result is immutable becauseof closure// but they also can be gotten by this.XXXlet _state = STATE_PENDING;let _result = undefined;Object.defineProperties(_self, {state: {get: () => _state,set: () => _state,enumerable: true},result: {get: () => _result,set: () => _result,enumerable: true}});_self.childStateControllerArray = [];_self.callbackArray = [];function handleStateChange(newState, result) {if (_state !== STATE_PENDING) {return;}_self.result = _result = result;_self.state = _state = newState;setTimeout(() => {if (!_self.callbackArray.length && newState === STATE_REJECTED) {handleUncaughtRejection(result);return;}_self.childStateControllerArray.forEach((child, index) => {handleChildWork.bind(_self)(child, _self.callbackArray[index]);});});}const resolve = curry(handleStateChange)(STATE_FULFILLED);const reject = curry(handleStateChange)(STATE_REJECTED);try {executor(resolve, reject);} catch (error) {reject(error);}}Promise.prototype.then = function (onFulfilled, onRejected) {return new Promise((next_res, next_rej) => {this.childStateControllerArray.push({resolve: next_res,reject: next_rej});this.callbackArray.push({ onFulfilled, onRejected });});};Promise.prototype.catch = function (onRejected) {return this.then(null, onRejected);};Promise.prototype.finally = function (onFinally) {this.isFinallyCb = true;return this.then(onFinally, onFinally);};export default Promise;
下面是的代码是reject、resovle、allSettled、all、race静态方法的实现。
const STATE_PENDING = "pending";const STATE_FULFILLED = "fulfilled";const STATE_REJECTED = "rejected";const checkIteratable = (data) => {if (typeof data[Symbol.iterator] !== "function") {throw new TypeError(`${typeof data} is not iterable`);}return true;};// this function will effect the source arrayconst waitAllPromisesSetteled = (promiseArray, callback) => {const settledArray = [];return new Promise((resolve, reject) => {function handlePromiseItemSettled() {settledArray.push(this);if (settledArray.length < promiseArray.length) {return;}callback({ settledArray, resolve, reject });}promiseArray.forEach((item, index, arr) => {// effect the source arrayif (!item instanceof Promise) {item = arr[index] = Promise.resolve(item);}if (item.state === STATE_PENDING) {const promiseCallback = handlePromiseItemSettled.bind(item);item.then(promiseCallback, promiseCallback);} else {settledArray.push(item);}if (settledArray.length === promiseArray.length) {callback({ settledArray, resolve, reject });}});});};Promise.resolve = function (data) {if (data instanceof Object && typeof data.then === "function") {data = new Promise(data.then);}if (data instanceof Promise) {return data;}return new Promise((res) => res(data));};Promise.reject = function (result) {return new Promise((res, rej) => rej(result));};Promise.race = function (promiseArray) {checkIteratable(promiseArray);return waitAllPromisesSetteled(promiseArray,({ settledArray, resolve, reject }) => {let fastestItem = settledArray[0];switch (fastestItem.state) {case STATE_REJECTED:reject(fastestItem.result);break;case STATE_FULFILLED:resolve(fastestItem.result);break;default:}});};Promise.all = function (promiseArray) {checkIteratable(promiseArray);if (!promiseArray.length) {return new Promise((res) => res([]));}const hasPromiseitem = promiseArray.every((item) => item instanceof Promise);return waitAllPromisesSetteled(promiseArray,({ settledArray, resolve, reject }) => {if (!hasPromiseitem) {setTimeout(resolve, 0, promiseArray);return;}const rejectedPromise = settledArray.find((item) => item.state === STATE_REJECTED);if (rejectedPromise) {reject(rejectedPromise.result);return;}const results = promiseArray.map((item) => item.result);resolve(results);});};Promise.allSettled = function (promiseArray) {checkIteratable(promiseArray);return waitAllPromisesSetteled(promiseArray, ({ resolve }) => {resolve(promiseArray);});};
完整代码在:
https://codesandbox.io/s/promise-3n8yf?file=/src/Promise.js
最后
感谢各位看官老爷在百忙之中抽出时间看我的文章
如果文中代码有和原生Promise表现不一致的地方欢迎大家评论,探讨
最后我再不要脸的要个赞吧
