【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面试题一次爽到底](
【JS】因两道Promise执行题让我产生自我怀疑,从零手写Promise加深原理理解的更多相关文章
- js 从两道面试题加深理解闭包与箭头函数中的this
壹 ❀ 引 在本文之前我已经花了两个篇幅专门介绍了JavaScript中的闭包与this,正好今早地铁上看到了两道面试题,试着做了下发现挺有意思,所以想单独写一篇文章来记录解析过程.若你对于闭包与t ...
- FJOI2020 的两道组合计数题
最近细品了 FJOI2020 的两道计数题,感觉抛开数据范围不清还卡常不谈里面的组合计数技巧还是挺不错的.由于这两道题都基于卡特兰数的拓展,所以我们把它们一并研究掉. 首先是 D1T3 ,先给出简要题 ...
- 又一道简单题&&Ladygod(两道思维水题)
Ladygod Time Limit: 3000/1000MS (Java/Others) Memory Limit: 65535/65535KB (Java/Others) Submit S ...
- 两道相似KMP题
1.POJ 3450 Coporate Identity 这两题的解法都是枚举子串,然后匹配,像这种题目以后可以不用KMP来做,直接字符串自带的strstr函数搞定,如果字符串未出现,该函数返回NUL ...
- 一道cf水题再加两道紫薯题的感悟
. 遇到一个很大的数除以另一个数时,可以尝试把这个很大的数进行,素数因子分解. . 遇到多个数的乘积与另一个数的除法时,求是否能整除,可以先求每一个数与分母的最大公约数,最后若分母数字为1,则证明可整 ...
- 算法(JAVA)----两道小小课后题
LZ最近翻了翻JAVA版的数据结构与算法,无聊之下将书中的课后题一一给做了一遍,在此给出书中课后题的答案(非标准答案,是LZ的答案,猿友们可以贡献出自己更快的算法). 1.编写一个程序解决选择问题.令 ...
- 超耐心地毯式分析,来试试这道看似简单但暗藏玄机的Promise顺序执行题
壹 ❀ 引 就在昨天,与朋友聊到JS基础时,她突然想起之前在面试时,遇到了一道难以理解的Promise执行顺序题.由于我之前专门写过手写promise的文章,对于部分原理也还算了解,出于兴趣我便要了这 ...
- JAVA算法两道
算法(JAVA)----两道小小课后题 LZ最近翻了翻JAVA版的数据结构与算法,无聊之下将书中的课后题一一给做了一遍,在此给出书中课后题的答案(非标准答案,是LZ的答案,猿友们可以贡献出自己更快 ...
- 手写promise
写在前面: 在目前的前端分开中,我们对于异步方法的使用越来越频繁,那么如果处理异步方法的返回结果,如果优雅的进行异步处理对于一个合格的前端开发者而言就显得尤为重要,其中在面试中被问道最多的就是对Pro ...
- js 关于setTimeout和Promise执行顺序问题
js 关于setTimeout和Promise执行顺序问题 异步 -- Promise和setTimeout 执行顺序 Promise 和 setTimeout 到底谁先执行 定时器的介绍 Jav ...
随机推荐
- MetaGPT day02: MetaGPT Role源码分析
MetaGPT源码分析 思维导图 MetaGPT版本为v0.4.0,如下是from metagpt.roles import Role,Role类执行Role.run时的思维导图: 概述 其中最重要的 ...
- asp.net core之Kestrel
简介 在ASP.NET Core中,Kestrel是一个重要的组件,它是一个跨平台的.开源的Web服务器,专门为ASP.NET Core应用程序而设计.Kestrel以其轻量级和高性能而闻名,本文将介 ...
- 08-逻辑仿真工具VCS-mismatch
逻辑仿真工具VCS mismatch,预计的仿真结果和实际仿真结果不同,寻找原因? 首先考虑代码,,不要让代码跑到工具的盲区中 其次考虑仿真工具的问题 +race -- 将竞争冒险的情况写到文件中 不 ...
- CLion创建自定义代码模板
1.问题 很多时候我们都想要简化代码编写,比如像IDEA那样,写入一个sout即会补全为System.out.println( |inserts cursor here| );的形式 最急切的例子便是 ...
- [转帖]Region is unavailable的排查总结
https://tidb.net/blog/07c99ed0#4%C2%A0%20%E4%B8%80%E4%BA%9B%E5%BB%BA%E8%AE%AE 1 region访问基本流程 tidb在访问 ...
- [转帖]TiKV集群搭建
https://www.cnblogs.com/luohaixian/p/15227788.html 1.准备环境 准备4台ubuntu 16.04虚拟机 部署规划: 节点类型 CPU 内存 存储 部 ...
- 【转帖】16.JVM栈帧内部结构-局部变量表
目录 1.局部变量表(Local variables) 1.局部变量表(Local variables) 1.局部变量表也称为局部变量数组或本地变量表. 2.局部变量表定义为一个数字数组,主要用于存储 ...
- [转帖] Linux命令拾遗-理解系统负载
https://www.cnblogs.com/codelogs/p/16060498.html 简介# 这是Linux命令拾遗系列的第七篇,本篇主要介绍Linux中负载的概念与问题诊断方法. 本系列 ...
- 除了Adobe之外,还有什么方法可以将Excel转为PDF?
前言 Java是一种广泛使用的编程语言,它在企业级应用开发中发挥着重要作用.而在实际的开发过程中,我们常常需要处理各种数据格式转换的需求.今天小编为大家介绍下如何使用葡萄城公司的的Java API 组 ...
- .Net Core 3.1浏览器后端服务(二) Web API项目分层
一.前言 分层开发的思想在计算机领域中至关重要,从操作系统到软件设计,分层思想无处不在. 在搭建项目的分层结构前,先简单了解下分层的优缺点.如下图,分为(呈现层.业务层.服务层.数据层) 分层的优点: ...