最近在准备面试,我发现有一道题叫如何实现一个Promise,也发现了网上好多实现的文章。但是代码都还挺长的,我记忆力不是很好,记不住那么多。
我害怕到时候面试官询问的时候,我一句话都说不出,遂写下此文。
Promise简单复习
看两段代码:
new Promise((resolve, reject)=>{
console.log(1);
})
// 1
new Promise((resolve, reject)=>{
console.log(1);
resolve();
}).then((res)=>{
console.log(2);
})
console.log(3);
// 1 3 2
好了,复习到此为止。
我们已经开始使用promise了。从上面的两段代码中我们可以获得以下信息:
- 构造Promise可以传入一个参数,这个参数是一个函数,这个函数在构造过程中就会被执行。
then中传入的回调会被异步执行。
我觉得应该先停下来一会儿,问自己一个问题:Promise是什么,它是一个什么定位?
Promise是一个对象,是一个中间对象。
这个中间对象是接收一个函数作为参数构造出来的。
这个中间对象有一个then方法,可以传入一个回调函数,也正像很多文章说的一样,这个回调函数将在promise决议的时候被调用。
再说的清晰一点,如果我们把构造promise对象传入的那个函数叫做A,把then传入的回调参数叫做B(当然这个B可能有很多个),那么promise是什么呢? Promise是一个中间对象,它位于函数A和函数B们的中间,是他们通信的基础媒介,一个妥妥的第三者。
函数A为什么要和函数B们通信呢?因为函数A可能是异步的,他可能要等拿到异步操作的结果后才能通知函数B们去执行。而在函数A通知函数B去执行之前,promise的状态是PENDING。
当函数A拿到想要的结果,并且想以这个结果通知函数B们的时候,有两种方式:resolve或者reject。这里或许我们还得把函数B们分个类,then的第一个参数处理的函数A通知的resolve结果的,第二个参数是处理函数A通知的reject结果的。
还需要注意一点,当函数A已经拿到结果后,调用resolve和reject并不是直接通知函数B们,它通知的是我们的中间者promise对象,通知它你的状态可以变成Fulfilled或者Rejected了。
好了,先到这里,我觉得我们可以开始实现promise了。
一步步实现Promise
上面我们知道了函数A在拿到想通知函数B们的结果后,会传递这些信息给中间对象Promise,并改变中间对象的状态。
// 先声明中间对象的三个可能的状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(f){
// state代表着中间对象的状态
this.state = PENDING
// 这个代表着函数A(此处是f)想告诉函数B们的结果.
this.result = null
const resolve = (result)=>{
// 这里写的是函数A拿到成功结果(感觉也不能叫成功结果...算了应该勉强能意会)后,想要对中间对象干滴好事...
}
const reject = (result)=>{
// 这里写的是函数A拿到坏蛋结果,想要对中间对象做点事~
}
f(resolve, reject)
}
这里我们已经写出来了一个大概的框架,当前的框架描述的还是函数A到中间对象的一些相关信息,暂时还没有涉及到函数B们。
接下来我们来补充resolve和reject里的逻辑,有一个逻辑肯定是共有的,那就是这两个操作都会改变中间对象的状态,并且把函数A拿到的决议结果缓存到中间对象。
所以我们定义一个transition函数来做这些事:
const transition = (promise, state, result)=>{
promise.state = state
promise.result = result
}
我们会这样调用transition:
const resolve = (result)=>{
transition(this, FULFILLED, result)
}
const reject = (result)=>{
transition(this, REJECTED, result)
}
好了,我们也应该思考一下关于中间对象到函数B们的逻辑了。加入函数B,我们可以使用中间对象的then方法,所以我们开始实现then。
// onFullfilled和onRejected都是回调函数,都是函数B们。
// 但是实际上,中间对象在状态决议后,仅会根据最终状态调用其中的一个。
// 所以不妨将onFullfilled和onRejected封装进同一个callback.
Promise.prototype.then = function(onFullfilled, onRejected){
const callback = {onFullfilled, onRejected};
}
先停一下,我们再回忆一下promise的行为。当函数A没有拿到决议结果时,我们当然可以对中间对象调用then注册回调,我们可以添加不止一个回调,最终当函数A得到决议结果通知中间对象promise时,会由中间对象按照之前then注册回调的顺序调用那些回调。所以,在中间对象的内部,我们还得有个属性用于缓存在函数A得到决议结果之前注册的回调。
function Promise(f){
//...
this.callbacks = [];
//...
}
我们在then中根据中间对象的当前状态进行合适的操作。
Promise.prototype.then = function(onFullfilled, onRejected){
const callback = {onFullfilled, onRejected};
if(this.state === PENDING){
// 此时函数A还没有拿到决议结果,所以我们将回调加入缓存
this.callbacks.push(callback)
}else{
// 此时函数A已经拿到决议结果。中间对象也已经缓存了决议结果,已经处于最终状态
// 因此,我们直接拿结果去处理这个callback,但是注意,得异步去处理,因为处于js环境,我们用setTimeout取模拟Promise的异步
// 注意Promise的异步和setTimeout的异步还是有区别的,更多话题请搜索: 宏任务和微任务.
setTimeout(()=> handleCallback(callback, this.state, this.result), 0)
}
}
我们来实现这个handleCallback函数:
const handleCallback = (callback, state, result)=>{
const {onFullfilled, onRejected} = callback
// 根据中间对象的最终状态调用合适的回调
if(state === FULFILLED){
onFullfilled(result)
}else if(state === REJECTED){
onRejected(result)
}
}
最后,我们不能忘了, 在中间对象promise刚拿到决议结果时,如果当前缓存回调的数组callbacks中有回调的时候,我们需要按顺序去处理这些回调:
const resolve = (result)=>{
transition(this, FULFILLED, result)
setTimeout(() => handleCallbacks(this.callbacks, this.state, this.result), 0)
}
const reject = (result)=>{
transition(this, REJECTED, result)
setTimeout(() => handleCallbacks(this.callbacks, this.state, this.result), 0)
}
const handleCallbacks = (callbacks, state, result)=>{
while(callbacks.length){
handleCallback(callbacks.shift(), state, result)
}
}
更多细节处理
上面已经是promise工作机制的一个最简单实现了,我们已经实现了函数A到中间对象,再到函数B们的一系列逻辑。
但是要真的实现一个符合Promise A+规范的,我们需要做的还有更多。
then的链式调用
promise是可以链式调用的,也就是then函数返回的还是一个promise,换句话说,then函数返回的依然是一个中间对象。
那这个中间对象是我们刚开始的那个中间对象吗?不妨写段代码验证一下:
const p1 = new Promise((resolve, reject)=>{
console.log(1);
})
const p2 = p1.then(()=>{
console.log(2);
})
console.log(p1 === p2) // false
结果不是,所以then函数返回的是一个新的中间对象。
我们把模型视角移到新的中间对象,问自己一个问题,对于这个中间对象来说,函数A是什么呢?自然还是不存在的,需要我们自己的定义。但有一个我们是知道的,为了实现then的链式调用,我们实际上要把上一个then中回调的结果作为下一个中间对象的决议结果。在这里,决议结果是onFulfilled或onRejected回调的结果。
我们可以很自然地去修改我们的代码了:
Promise.prototype.then = function (onFullfilled, onRejected) {
return new Promise((resolve, reject) => {
// callback中存储一下和新的中间对象的联系
const callback = { onFullfilled, onRejected, resolve, reject };
if (this.state === PENDING) {
this.callbacks.push(callback)
} else {
setTimeout(() => handleCallback(callback, this.state, this.result), 0)
}
});
};
const handleCallback = (callback, state, result) => {
let { onFulfilled, onRejected, resolve, reject } = callback
if (state === FULFILLED) {
resolve(onFulfilled(result))
} else if (state === REJECTED) {
reject(onRejected(result))
}
}
如果then注册的不是函数
如果then注册的回调不是函数,那么then返回的新的中间对象的决议值是上一个中间对象的决议值。非常简单。
const isFunction = (obj) => typeof obj === 'function';
const handleCallback = (callback, state, result) => {
let { onFulfilled, onRejected, resolve, reject } = callback
if (state === FULFILLED) {
isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
} else if (state === REJECTED) {
isFunction(onRejected) ? reject(onRejected(result)) : reject(result)
}
}
如果函数执行过程中发生错误,立刻使当前promise决议成rejected
这个执行的函数实际上说的是函数A。
我们需要对两处地方做出修改:
// 此处针对的是最开始那个中间对象
function Promise(f){
// ...
try{
f(resolve, reject)
}catch(e){
reject(e)
}
}
// 此处针对then 返回的那个中间对象
const handleCallback = (callback, state, result) => {
let { onFulfilled, onRejected, resolve, reject } = callback
try {
if (state === FULFILLED) {
isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result)
} else if (state === REJECTED) {
isFunction(onRejected) ? resolve(onRejected(result)) : reject(result)
}
} catch (error) {
reject(error)
}
};
校验决议结果
在上面,我们一直假定决议结果是一个普通的值,事实上针对这个决议结果,我们还需要做一些校验:
const isObject = (obj) => !!(obj && typeof obj === 'object');
const isThenable = (obj) => (isFunction(obj) || isObject(obj)) && 'then' in obj;
const isPromise = (promise) => promise instanceof Promise;
const resolvePromise = (promise, result, resolve, reject) => {
if (result === promise) {
// 这个函数A得到的决议结果不能是当前这个中间对象
let reason = new TypeError('Can not fulfill promise with itself.');
return reject(reason);
}
if (isPromise(result)) {
// 如果得到的决议结果是一个新的promise(中间对象),
// 那么当前的中间对象的最终决议结果取决于新的中间对象的决议结果
return result.then(resolve, reject);
}
if (isThenable(result)) {
// 如果结果是一个包含then属性的对象,则进行展开
try {
let then = result.then;
if (isFunction(then)) {
return new Promise(then.bind(result)).then(resolve, reject);
}
} catch (err) {
return reject(err);
}
}
// 如果不是上述情况,则直接resolve result
resolve(result);
}
防止重复决议,加一层锁
我们把更改中间状态的两把钥匙resolve和reject都交给了函数A,我们不知道函数A会怎么使用这两把钥匙。
有个经典的问题,函数A的逻辑中,resolve(...)或reject(...)后面的逻辑会执行吗?
直接写代码验证一下:
new Promise((resolve) => {
resolve(2);
console.log(1);
}).then((res) => {
console.log(res);
});
// 1 2
其实根据我们之前的实现逻辑也非常好理解,函数A就是一个正常的函数,resolve(...)或reject(...)后面的逻辑都会正常执行。那么理所当然,我们可能在调用resolve后再调用reject,这会多次更改中间对象promise的最终状态。规范不允许我们这么做,promise不能重复决议。
所以我们加一个锁:
const transition = (promise, state, result) => {
if (promise.state !== PENDING) return;
promise.state = state;
promise.result = result;
// 将之前的逻辑合并到transition中
setTimeout(() => handleCallbacks(promise.callbacks, state, result), 0);
};
function Promise(f) {
this.result = null;
this.state = PENDING;
this.callbacks = [];
let onFulfilled = (value) => transition(this, FULFILLED, value);
let onRejected = (reason) => transition(this, REJECTED, reason);
// 使用ignore来保证中间对象只能决议一次.
let ignore = false;
let resolve = (value) => {
if (ignore) {
return;
}
ignore = true;
resolvePromise(this, value, onFulfilled, onRejected);
};
let reject = (reason) => {
if (ignore) {
return;
}
ignore = true;
onRejected(reason);
};
try {
f(resolve, reject);
} catch (e) {
reject(e);
}
}
总结
至此,我们的promise实现已经一步步添砖加瓦实现了,其实这是工业聚大佬的实现,这篇文章的后半段其实比前半段更有价值。
最后的最后,我会问问自己,在实现Promise过程中什么才是重要的?
是最终的实现代码吗?不是的。
重要的,是对于Promise作为一个中间对象的理解,是代码中闭包的巧妙运用,是一种有关于设计模式的思考,是关于各种边界情况的深思。