【JS】因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解
壹 ❀ 引
其实在去年七月份,博客所认识的一个朋友问了我一个关于Promise
执行先后的问题,具体代码如下:
const fn = (s) => (
new Promise((resolve, reject) => {
if (typeof s === 'number') {
resolve();
} else {
reject();
}
})
.then(
res => console.log('参数是一个number'),
)
.catch(err => console.log('参数是一个字符串'))
)
fn('1');
fn(1);
// 先输出 参数是一个number
// 后输出 参数是一个字符串
他的疑惑是,以上代码中关于Promise
状态的修改都是同步的,那为什么fn(1)
的输出还要早于fn('1')
?
说来惭愧,我当时对于这个输出也疑惑了半天,最后基于自己掌握的现有知识,给了对方一个自认为说的过去但现在回想起来非常错误的解释...想起来真是羞愧= =,这个问题也让我当时有了了解Promise
底层原理的想法。
没过多久,另一位博客认识的朋友又问了我一道Promise
执行顺序的题,代码如下:
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() =>{
console.log(6);
})
// 输出为0 1 2 3 4 5 6
我看了一眼题,结果难道不应该是0 1 4 2 3 5 6
?对方抱着疑问而来,结果这次我自己都蒙圈了,这也让我意识到自己对于Promise
的理解确实有点薄弱。
我承认,上面两道题真的有点为考而考的意思了,毕竟实际开发我们也不可能写出像例子2这样的代码,但站在面试的角度,对方总是需要一些评判标准来筛掉部分人,人人都不想卷,却又不得不卷,多懂一点总是没有坏处。
既然意识到自己的不足,那就花点功夫去了解Promise
原理,如何了解?当然是模拟实现一个Promise
,所以本篇文章的初衷是通过手写Promise
的过程理解底层到底发生了什么,从而反向解释上面两道题为什么会这样。放心吧,当我写完我已经恍然大悟,所以你也一定可以,那么本文开始。
贰 ❀ 从零手写Promise
贰 ❀ 壹 搭建框架
对于手写新手而言,从零开始写一个Promise
真正的难点在于你可能不清楚到底要实现Promise
哪些特性,没事,我们从一个最简单的例子开始分析:
const p = new Promise((resolve, reject) => {
// 同步执行
resolve(1);
});
p.then(
res => console.log(res),
err => console.log(err)
);
从上述代码我们可以提炼出如下信息:
new
过程是同步的,我们传递了一个函数(resolve, reject)=>{resolve(1)}
给Promise
,它会帮我们同步执行这个函数。- 我们传递的函数接受
resolve reject
两个参数,这两个参数由Promise
提供,所以Promise
一定得有这两个方法。 new Promise
返回了一个实例,这个实例能调用then
方法,因此Promise
内部一定得实现then
方法。
我们也别想那么多,先搭建一个基本的Promise
框架,代码如下:
class MyPromise {
constructor(fn) {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
}
resolve = () => {}
reject = () => {}
then = () => {}
}
在constructor
中接受的参数fn
其实就是new Promise
传递的函数,我们在constructor
中同步调用它,同时传递了this.resolve
与this.reject
,这也就解释了为何传递的函数会同步执行,以及如何使用到Promsise
提供的resolve
方法。
贰 ❀ 贰 增加状态管理与值记录
我们知道Promise
有pending、fuldilled、rejected
三种状态,且状态一旦改变就无法更改,无论成功失败或者失败,Promise
总是会返回一个succesValue
或者failReason
回去,所以我们来初始化状态、value以及初步的成功/失败逻辑:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(fn) {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
}
// 初始化状态以及value
status = PENDING;
value = null;
resolve = (value) => {
// 当调用resolve时修改状态成fulfilled,同时记录成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject = (reason) => {
// 当调用reject时修改状态成rejected,同时记录失败的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
}
}
then = () => {}
}
叁 ❀ 叁 初步实现then
在实现Promise
状态管理以及值记录后,我们接着来看看then
,很明显then
接受两个参数,其实就是成功的与失败的回调,而这两个函数我们也得根据之前的this.status
来决定要不要执行,直接上代码:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(fn) {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
}
// 初始化状态以及成功,失败的值
status = PENDING;
value = null;
resolve = (value) => {
// 当调用resolve时修改状态成fulfilled,同时记录成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject = (reason) => {
// 当调用reject时修改状态成rejected,同时记录失败的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
}
}
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn
};
callbackMap[this.status](this.value);
}
}
那么到这里我们已经实现了一个简陋的MyPromise
,让我们检验下状态改变以及回调执行:
const p = new MyPromise((resolve, reject) => {
// 同步执行
resolve(1);
reject(2);
});
p.then(
res => console.log(res),
err => console.log(err)
);
// 只输出了1
上述代码只输出了1,说明状态控制以及回调处理都非常成功!!!我们继续。
贰 ❀ 肆 异步修改状态
上述代码虽然运行正常,但其实只考虑了同步resolve
的情况,假设我们修改状态在异步上下文中,就会引发意想不到的错误,比如:
const p = new MyPromise((resolve, reject) => {
// 同步执行
setTimeout(() => resolve(1), 2000);
});
p.then(
(res) => console.log(res),
(err) => console.log(err)
);
Uncaught TypeError: callbackMap[this.status] is not a function
简单分析下,因为目前我们对于Promise
状态的修改依赖了resolve
,但因为定时器的缘故,导致执行p.then
执行时状态其实还是pending
,从而造成callbackMap[this.status]
无法匹配,因此我们需要添加一个pending
状态的处理。
还有个问题,即使解决了callbackMap
匹配报错,定时器等待结束后执行resolve
,我们怎么再次触发对应回调的执行呢?要不我们在pending
状态中把两个回调记录下来,然后在resolve
或者reject
时再调用记录的回调?说干就干:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(fn) {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
}
// 初始化状态以及成功,失败的值
status = PENDING;
value = null;
// 新增记录成功与失败回调的参数
fulfilledCallback = null;
rejectedCallback = null;
resolve = (value) => {
// 当调用resolve时修改状态成fulfilled,同时记录成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回调的调用
this.fulfilledCallback?.(value);
}
};
reject = (reason) => {
// 当调用reject时修改状态成rejected,同时记录失败的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失败回调的调用
this.rejectedCallback?.(reason);
}
};
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback = fulfilledFn;
this.rejectedCallback = rejectedFn;
},
};
callbackMap[this.status](this.value);
};
}
再次执行上面定时器的例子,现在不管有没有异步修改状态,都能正常执行了!!!
贰 ❀ 伍 实现then多次调用
当我们new
一个Promise
后会得到一个实例,而这个实例其实是支持多次then
调用的,比如:
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 0);
});
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
但如果我们我们使用自己实现的MyPromise
去做相同的调用,你会发现只会输出1个1,原因也很简单,我们在pending
情况下记录回调的逻辑只能记录一个,所以还得再改造一下,将fulfilledCallback
定义成一个数组,如下:
class MyPromise {
// ....
// 修改为数组
fulfilledCallback = [];
rejectedCallback = [];
resolve = (value) => {
// 当调用resolve时修改状态成fulfilled,同时记录成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回调的调用
while (this.fulfilledCallback.length) {
this.fulfilledCallback.shift()?.(value);
}
}
};
reject = (reason) => {
// 当调用reject时修改状态成rejected,同时记录失败的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失败回调的调用
while (this.rejectedCallback.length) {
this.rejectedCallback.shift()?.(reason);
}
}
};
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
callbackMap[this.status](this.value);
};
}
这也修改完成后再次执行上述例子,我们发现多次调用then
已满足。
贰 ❀ 陆 实现then链式调用
OK,终于来到Promise
链式调用这个环节了,对于整个Promise
手写,我个人觉得这部分是稍微有点绕,不过我会尽力解释清楚,我们先看个最简单的例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(2));
}).then((res) => {
console.log(res);
});
// 1
// 2
假设我们将上述代码中的new Promise
都改为new MyPromise
,运行你会发现代码直接报错:
Uncaught TypeError: Cannot read properties of undefined (reading 'then')
不能从undefined
上读取属性then
?我不是在then
里面return
了一个new Promise
吗?这咋回事?假设你是这样想的,那么恭喜你,你已经成功进入了思维误区。
我们将上面的例子代码进行拆分:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(2));
});
p2.then((res) => {
console.log(res);
});
Promise
若要实现链式调用,那么p1.then()
一定得返回一个新的Promise
,不然下一次链式调用的then
从哪读取呢?
所以这个新的Promise
是then
方法创建并提供的,而(res)=>{console.log(1);return new Promise((resolve) => resolve(2))}
这一段只是then
方法调用时的callback
,它的返回值(假设有值)将成为下次新的Promise
的value
,所以上述代码中的return new Promise((resolve) => resolve(2))
只是在为then
创建的Promise
准备value
而已。看个例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return new Promise((resolve) => {
// 我们不改状态
console.log("不做状态改变的操作");
});
}).then((res) => {
console.log(res); // 这里不会输出
});
在这个例子中,第二个then
并不会执行,这是因为p1.then()
虽然创建了一个新的Promise
,但是它依赖的value
由内部的new Promise
提供,很明显我们并未做任何状态改变的操作,导致第二个Promise
不会执行。
那么到这里我们能提炼出两个非常重要的结论:
Promise
若要实现链式调用,then
一定得返回一个新的Promise
。- 新的
Promise
的状态以及value
由上一个then
的callback
决定。
再次回到我们自己实现的then
方法,很明显它并没有创建一个新Promise
,函数没返回值默认返回undefined
,这就解释了为啥报这个错了。
好了,解释完了我们得再次改造我们的MyPromise
,为then
提供返回Promise
的操作,以及对于then
的callback
结果的处理:
const resolvePromise = (result, resolve, reject) => {
// 判断result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
// ....
then = (fulfilledFn, rejectedFn) => {
// 我们得在每次调用then时返回一个Promise
return new MyPromise((resolve, reject) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
// 上一个then的callback的结果将作为新Promise的值
const result = callbackMap[this.status](this.value);
resolvePromise(result, resolve, reject);
});
};
}
经过这样修改,再次运行代码,我们发现then
链式调用已经成功了!!!!
我知道上面这段代码有同学又懵了,我建议先看看上面对于then
链式调用我们得出的两个结论,然后我再用两个例子来解释这段代码为什么要这样写,别着急,我会解释的非常清楚。
对于then
返回一个Promise
的修改这一点大家肯定没问题,疑惑的点应该都在新增的resolvePromise
方法中。其实在前面我们解释过了,第一个then
回调返回结果(函数没返回默认就是undefined
),会作为下一个新Promise
的参数,而这个返回的结果它可能是一个数字,一个字符串,也可能是一个Promise
(上面的例子就是返回了一个promise
作为参数),先看一个简单的例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
return 520;
}).then((res) => {
console.log(res);// 520
});
这个例子的第一个then
的callback
直接返回了一个数字,但奇怪的是下一个then
居然能拿到这个结果,这是因为上述代码等同于:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
return Promise.resolve(520);
}).then((res) => {
console.log(res);// 520
});
没错,这也是Promise
的特性之一,如果我们的then
的回调返回的是一个非Promise
的结果,它等同于执行Promise.resolve()
,这也是为啥我们在自定义的resolvePromise
中一旦判断result
不是Promise
就直接执行resolve
的缘故。
强化理解,来给大家看个更离谱的例子:
Promise.resolve()
.then(() => {
return new Error("error!!!");
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失败啦");
});
猜猜这段代码最终输出什么?输出成功啦
,因为它等同于:
Promise.resolve()
.then(() => {
return Promise.resolve(new Error("error!!!"));
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失败啦");
});
对于Promise
而言,它只是一个type
类型是错误的value
而已,当然执行成功回调。有同学可能就要问了,那这个例子假设我就是想执行catch
咋办?两种写法:
Promise.resolve()
.then(() => {
// 第一种办法,直接reject
return Promise.reject(new Error("error!!!"));
// 第二种办法,直接抛出错误
// throw new Error('error!!!')
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失败啦");
});
解释了resolvePromise
中的resolve(result)
,再来解释下为什么result
是Promise
时执行result.then(resolv,reject)
就可以了。
我们已知回调的结果会作为下一个Promise
的参数,那假设这个参数自身就是个Promise
,对于then
返回的新Promise
而言,它就得等着作为参数的Promise
状态改变,在上面我们已经演过参数是Promise
但不会改变状态的例子,结果就是下一个then
不会调用。
所以对于下一个新Promise
而言,我就等着参数自己送到嘴里来,你状态变不变,以及成功或者失败那是你自己的事,因此我们通过result.then()
来等待这个参数Promise
的状态变化,只要你状态变了,比如resolve
了,那是不是得执行this.resolve
方法,从而将值赋予给this.value
,那么等到下一次执行then
时自然就能读取对应this.value
了,是不是很巧妙?
另外,result.then(resolve, reject);
这一句代码其实是如下代码的简写,不信大家可以写个小例子验证下:
result.then((res)=> resolve(res), (err)=> reject(err));
算了,我猜测你们可能还是懒得写例子验证,运行下如下代码就懂了,其实是一个意思:
// 定时器是支持传递参数的
setTimeout(console.log, 1000, '听风是风')
// 等同于
setTimeout((param)=> console.log(param), 1000, '听风是风')
那么上面的简写,其实也是这个意思,然后我们画张图总结下上面的结论:
恭喜你,模拟Promise
最为绕的一部分你弄清楚了,剩下的模拟都是小鱼小虾,我们继续。
贰 ❀ 柒 增加then不能返回Promise自己的判断
直接看个例子,这个代码执行报错:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return p2;
});
Uncaught (in promise) TypeError: Chaining cycle detected for promise #
结合上面我们自己实现then
的理解,p1.then()
返回了一个Promise p2
,结果p2
又成p2
自己需要等待的参数,说直白点就是p2
等待p2
的变化,自己等自己直接陷入死循环了。对于这个问题感兴趣的同学可以看看segmentfault中对于这个问题的解答 关于promise then的问题。
我们也来模拟这个错误的捕获,直接上代码:
const resolvePromise = (p, result, resolve, reject) => {
// 判断是不是自己,如果是调用reject
if (p === result) {
reject(new Error("Chaining cycle detected for promise #<Promise>"));
}
// 判断result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
// ....
then = (fulfilledFn, rejectedFn) => {
// 我们得在每次调用then时返回一个Promise
const p = new MyPromise((resolve, reject) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
// 上一个then的callback的结果将作为新Promise的值
const result = callbackMap[this.status](this.value);
// 新增了一个p,用于判断是不是自己
resolvePromise(p, result, resolve, reject);
});
return p;
};
}
执行上面的代码,结果又报错....
index.html:159 Uncaught ReferenceError: Cannot access 'p2' before initialization
错误说我们不能在p2
初始化好之前调用它,其实看上面那个代码本身就很奇怪,哪有在产生自己的函数的callback
中使用自己的,但这就是Promise
的特性之一,咱也没办法。
现在思路就是让resolvePromise(p, result, resolve, reject)
这一句执行晚一点,起码要晚于新Promise
的产生,咋办?当然是用异步,比如定时器。但我们知道Promise
的then
是微任务,为了更好的模拟这个异步行为,这里借用一个API,名为queueMicrotask,想详细了解的同学可以点击链接跳转MDN,这里我们直接上个简单的例子:
queueMicrotask(() => {
console.log("我是异步的微任务");
});
setTimeout(() => console.log("我是异步的宏任务"));
console.log("我是同步的宏任务");
看来这个API非常符合我们的预期,因为需要考虑pending
状态暂存函数的行为,我们还是额外封装两个成功与失败的微任务,继续改造:
then = (fulfilledFn, rejectedFn) => {
// 我们得在每次调用then时返回一个Promise
const p = new MyPromise((resolve, reject) => {
// 封装成功的微任务
const fulfilledMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
// 获取成功回调函数的执行结果
const result = fulfilledFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, result, resolve, reject);
});
};
// 封装失败的微任务
const rejectedMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
// 调用失败回调,并且把原因返回
const result = rejectedFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, result, resolve, reject);
});
};
const callbackMap = {
[FULFILLED]: fulfilledMicrotask,
[REJECTED]: rejectedMicrotask,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledMicrotask);
this.rejectedCallback.push(rejectedMicrotask);
},
};
callbackMap[this.status]();
});
return p;
};
好了,现在执行下面这段代码来检验下效果:
const p1 = new MyPromise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return p2;
});
p2.then(
() => {},
(err) => console.log(err)
);
有同学就要说了,你这不对啊,原生Promise
是直接就报错,你这还要p2.then()
才能感知报错。咱前面就说了,这是在模拟仿写Promise
,大致达到这个效果,而且这个小节的核心目的其实是为了引出then
中callback
执行为什么是异步的原因。
贰 ❀ 捌 添加new Promise以及then执行错误的捕获
我们知道new Promise
或者then
回调执行报错是,then
的错误回调是能成功捕获的,我们也来模拟这个过程,这个好理解一点我们就直接上代码:
class MyPromise {
constructor(fn) {
try {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
// ....
then = (fulfilledFn, rejectedFn) => {
// 我们得在每次调用then时返回一个Promise
const p = new MyPromise((resolve, reject) => {
// 封装成功的微任务
const fulfilledMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
// 添加错误捕获
try {
// 获取成功回调函数的执行结果
const result = fulfilledFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
});
};
// 封装失败的微任务
const rejectedMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
// 添加错误捕获
try {
// 调用失败回调,并且把原因返回
const result = rejectedFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
});
};
// ....
});
return p;
};
}
执行如下例子,效果很理想:
const p1 = new MyPromise((resolve, reject) => {
throw new Error("new报错啦");
});
const p2 = p1.then(
(res) => {
console.log(res);
},
(err) => {
console.log("我是错误回调", err);
throw new Error("then报错啦");
}
);
p2.then(
() => {},
(err) => console.log("我是错误回调", err)
);
贰 ❀ 玖 实现then无callback,或者callback不是函数时的值穿透
看标题可能不明白什么意思,看个例子就懂了:
const p1 = new Promise((resolve, reject) => {
resolve("听风");
});
const fn = () => {};
p1.then(fn()) // 函数调用,并不是一个函数
.then(1) // 数字
.then('2') // 字符串
.then() // 不传递
.then((res) => console.log(res)); // 听风
说通俗一点就是,假设then
没有回调,或者回调根本不是一个函数,那么你就当这个then
不存在,但我们的MyPromise
很明显没考虑无回调的情况,现在实现这一点:
then = (fulfilledFn, rejectedFn) => {
// 新增回调判断,如果没传递,那我们就定义一个单纯起value接力作用的函数
fulfilledFn =
typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
rejectedFn =
typeof rejectedFn === "function"
? rejectedFn
: (value) => {
throw value;
};
// 我们得在每次调用then时返回一个Promise
const p = new MyPromise((resolve, reject) => {
// ...
});
return p;
};
上述代码做的事情非常简单,检查两个回调是不是函数,不是函数我们就帮它定义一个只做值接力的函数,你传递什么我们就原封不动返回什么的函数。为啥rejectedFn
要定义成(value)=>{throw value}
呢?这是因为我们希望当此函数执行时能走reject
路线,所以一定得抛错,那为什么不写成(value)=>{throw new Error(value)}
这样?因为.then().then()
这种会导致new Error
执行多次,结果就不对了。我们在贰 ❀ 陆小节,提到有两种办法可以在报错时让catch
捕获,一种是直接reject()
,另一种就是throw
一个错误,后面的throw
影响更小一点,所以就用这种。
经过上面的修改,此时我们再执行我们无回调的例子,此时不管是成功还是失败,都能成功执行了。
贰 ❀ 拾 实现静态resolve与reject
创建Promise
除了new Promise
之外,其实还能通过Promise.resolve()
静态方法直接获取,但目前MyPromise
只提供了实例方法,所以我们需要补全静态方法:
class MyPromise {
// ....
// 静态resolve
static resolve(value) {
// 加入蚕食是一个promise,原封不动的返回
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
// 静态reject
static reject(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
reject(value);
});
}
// ....
}
逻辑也很简单,如果参数是一个Promise
,那就原封不动返回,如果不是,我们就手动帮他创建一个Promise
即可,这个特性可以通过下面这个例子验证:
const p1 = new Promise((resolve, reject) => {
resolve("听风");
});
const p2 = new Promise((resolve, reject) => {
resolve("我是一个promise");
});
p1.then(
(res) => {
return Promise.resolve(p2);
},
(err) => console.log(err)
).then(
(res) => console.log(res), // 我是一个promise
(err) => console.log(err)
);
可以看到假设Promise.resolve
参数本身就是一个Promise
时,这个方法本质上就想啥也没做一样,但如果参数是一个数字,它会帮你包装成一个Promise
,我们将上述代码的new Promise
改成new MyPromise
,效果完全一致,说明模拟的很理想!!
OK,那么到这里,一个满足基本功能的MyPromise
就实现完毕了,但事先说明,它并未符合Promise A+
规范,如果要做到一样,我们仍需要对then
方法中做一些条件判断,这些逻辑都是规范明确告诉你应该怎么写,没有什么道理可言,但鉴于这段逻辑补全对于我们理解上面的题不会有额外的帮助,因此我就不做额外的改造了,下面是一份实现到现在完整的MyPromise
代码:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
const resolvePromise = (p, result, resolve, reject) => {
if (p === result) {
reject(new Error("Chaining cycle detected for promise #<Promise>"));
}
// 判断result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
constructor(fn) {
try {
// 这里的fn其实就是new Promise传递的函数
fn(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
// 初始化状态以及成功,失败的值
status = PENDING;
value = null;
// 新增记录成功与失败回调的参数
fulfilledCallback = [];
rejectedCallback = [];
// 静态resolve
static resolve(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
// 静态reject
static reject(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
reject(value);
});
}
resolve = (value) => {
// 当调用resolve时修改状态成fulfilled,同时记录成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回调的调用
while (this.fulfilledCallback.length) {
this.fulfilledCallback.shift()?.(value);
}
}
};
reject = (reason) => {
// 当调用reject时修改状态成rejected,同时记录失败的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失败回调的调用
while (this.rejectedCallback.length) {
this.rejectedCallback.shift()?.(reason);
}
}
};
then = (fulfilledFn, rejectedFn) => {
// 新增回调判断,如果没传递,那我们就定义一个单纯起value接力作用的函数
fulfilledFn =
typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
rejectedFn =
typeof rejectedFn === "function"
? rejectedFn
: (value) => {
throw value;
};
// 我们得在每次调用then时返回一个Promise
const p = new MyPromise((resolve, reject) => {
// 封装成功的微任务
const fulfilledMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
try {
// 获取成功回调函数的执行结果
const x = fulfilledFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
// 封装失败的微任务
const rejectedMicrotask = () => {
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
try {
// 调用失败回调,并且把原因返回
const x = rejectedFn(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
const callbackMap = {
[FULFILLED]: fulfilledMicrotask,
[REJECTED]: rejectedMicrotask,
// 针对异步问题,新增pending状态时记录并保存回调的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledMicrotask);
this.rejectedCallback.push(rejectedMicrotask);
},
};
callbackMap[this.status]();
});
return p;
};
}
代码看着有点多,但事实上顺着思路写下来,其实没有什么很大的难点。
叁 ❀ 重回面试题
MyPromise
实现完毕,现在让我们回头再看看第一道题,现在再来分析为什么这么输出,为了方便,我将题目加在下方:
const fn = (s) => (
new Promise((resolve, reject) => {
if (typeof s === 'number') {
resolve();
} else {
reject();
}
})
.then(
res => console.log('参数是一个number'),
// 注意,这里没定义失败回调
)
.catch(err => console.log('参数是一个字符串'))
)
fn('1');
fn(1);
叁 ❀ 壹 第一轮执行
我们先考虑同步执行,首先我们执行fn('1')
,此时执行new Promise
,因为这个过程是一个同步行为,因此它会立马调用传递给Promise
的回调,然后走逻辑判断,因为不是一个数字,导致执行了reject()
。
紧接着执行.then
,前文也说了.then
注册微任务的行为是同步,但需要注意的是,.then
中并未提供失败函调,因此对于Promise
底层而言,它要做的是值和状态的穿透,这些先不管,毕竟我们还有剩余的同步任务没走完。
于是紧接着,我们又执行了fn(1)
,同样同步执行.then()
注册了成功的回调,到这里,同步任务全部执行完成。
叁 ❀ 贰 第二轮执行
由于同步代码全部跑完了,此时肯定得按照我们注入的微任务顺序,依次执行微任务,由于fn('1')
这一步的.then()
没有失败回调,默认理解为执行了值穿透的步骤,于是返回的新Promise
的状态依旧是rejected
且值为undefined
(因为reject
没传值)。
紧接着,我们执行fn(1)
的成功回调,于是先输出了参数是一个number
,注意,这个成功回调只有一个console
,并无返回,我们默认理解为return resolve(undefined)
,因此返回了一个状态是成功,但是值是undefined
的新Promise
。
叁 ❀ 叁 第三轮执行
两次调用的.then
又返回了两个新promise
,因为状态一开始都改变了,所以还是先走rejected
的Promise
,并成功触发.catch
,此时输出参数是一个字符串
,而第二个Promise
是成功状态,不能触发.catch
,到此执行结束。
为了更好理解值穿透的解释,我们改改代码:
const fn = (s) => {
new Promise((resolve, reject) => {
if (typeof s === "number") {
resolve(1);
} else {
reject(2);
}
})
.then(
(res) => console.log("参数是一个number")) // 注意,这里虽然提供了函数,但是没返回,所以理解为 return resolve(undefined)
// 注意,这里没传递失败函数,只要callback不是一个函数,默认值穿透拿上一步的promise
.then(
(succ) => console.log(succ) // 这里一定输出undefined,毕竟上一步没返回值,默认理解成resolve(undefined)
)
.catch((err) => {
console.log("参数是一个字符串");
console.log(err); // 这里输出2,因为上一个then又没失败回调,一直穿透下来
});
};
fn("1");
fn(1);
// 参数是一个number
// undefined
// 参数是一个字符串
// 2
而假设我们有为then
提供失败回调,那么此时返回的顺序就符合一开始我们对于Promise
还不太了解时能够理解的预期:
const fn = (s) => {
new Promise((resolve, reject) => {
if (typeof s === "number") {
resolve();
} else {
reject();
}
})
.then(
(res) => console.log("参数是一个number"),
(err) => console.log("参数是一个字符串11")
)
.catch((err) => {
console.log("参数是一个字符串");
// 看看上一个then传递的value是啥
console.log(err);
});
};
fn("1");
fn(1);
因为有提供失败回调,这就导致.catch
不会执行了。那么到这里,第一道面试题算是非常透彻的解释完了,也多亏手写Promise
加深了对于底层原理的理解。
我们接着聊第二道题,为了方便理解,我们将这道题的Promise
全部改成MyPromise
,再看看输出如何:
MyPromise.resolve()
.then(() => {
console.log(0);
return MyPromise.resolve(4);
})
.then((res) => {
console.log(res);
});
MyPromise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
// 0 1 2 4 3 5 6
使用我们实现的MyPromise
,结果发现4
跑到了2
后面,我们可以先站在自己实现的逻辑上解释这个现象。
我们已知.then()
会返回一个Promise
,且这个Promise
啥时候执行以及参数都是由.then()
接收的回调函数的返回结果来决定的。而在题目中MyPromise.resolve(4)
这一句,其实本质上就等同于如下代码(参照静态resolve
实现):
MyPromise.resolve()
.then(() => {
console.log(0);
return new MyPromise(resolve=>resolve(4));
})
.then((res) => {
console.log(res);
});
而在then
调用中最后都需要走resolvePromise
,此方法会判断参数是否是一个Promise
,如果是就需要执行result.then()
。
不知道你脑袋里是否已经有了一种感觉,相比.then(()=>console.log(2))
,前者比后者多执行了一次.then
,也就是说多创建了一次微任务,这就导致4
一定晚于2
输出。
但是题目2的输出,4
其实是在3
之后,会不会有一种可能,官方Promise
中return Promise.resolve(4)
这种行为在底层其实创建了两次微任务,导致4延迟了2次后才输出呢?
在查证了V8中关于Promise
的源码,直接说结论,确实是创建了两次微任务,因为涉及到篇幅问题,若对这两个微任务有兴趣,可直接阅读知乎问题 promise.then 中 return Promise.resolve 后,发生了什么?,有优秀答主详细分析了源码中两次微任务产生的地方,只是站在我的角度,我个人觉得了解到这个结论就好,再继续深入分析收益不成正比,所以在这我偷个懒。
肆 ❀ 总
那么到这里,一篇长达八千多字的文章也记录完成了,本着了解两道面试题的态度,我们尝试手写了一个自己的Promise
,在实现过程中,就我自己而言确实又了解了不少之前从未听过的特性,比如Promise
不能返回自己,比如.then
返回的Promise
的执行其实依赖了.then
回调函数的结果等等。另外,我会在参考中附带一篇我觉得很不错的Promise
面试题集合,大家也可以在看完这篇文章后尝试做做这里面的执行题,加深对于Promise
的理解。
另外,实际面试中基本没有真让你手写Promise A+
的题,毕竟规范那么多,手写下来难度过大,但实际面试会有让你手写Promise.all
或者Promise.race
类似的手写题,后续我也会把这些手写问题给补全,那么到这里本文结束。
推荐阅读
超耐心地毯式分析,来试试这道看似简单但暗藏玄机的Promise顺序执行题
一个思路搞定三道Promise并发编程题,手摸手教你实现一个Promise限制器
强化Promise理解,从零手写属于自己的Promise.all与Promise.race
伍 ❀ 参考
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
【V8源码补充篇】从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
promise.then 中 return Promise.resolve 后,发生了什么?
[要就来45道Promise面试题一次爽到底](