我的测试环境: 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 = this
let _state = STATE_PENDING
let _result = undefined
Object.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 function
console.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、reject
function 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 callback
try {
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 Promise
if (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 setTimeout
const _self = this;
// state & result is immutable becauseof closure
// but they also can be gotten by this.XXX
let _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 callback
try {
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 Promise
if (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 setTimeout
const _self = this;
// state & result is immutable becauseof closure
// but they also can be gotten by this.XXX
let _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 array
const 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 array
if (!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表现不一致的地方欢迎大家评论,探讨
最后我再不要脸的要个赞吧