持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
前言
写这篇文章的目的:
- 在各大论坛其实有很多文章都在讲解
Promise的面试题,但是这些文章的观点很多都是不完全正确的,甚至有些理论都不自圆其说 - 恰好,最近我也在研究这方面的问题,本文通过 真实论据,实例演示 分析这类问题
- 本文虽然涉及 v8 引擎 层面的分析,但作者会尽可能描述地通俗易懂
在阅读这篇文章之前希望你已经对 Promise 基本使用、 包括 异步模型 有一定的了解,否则会影响一定的阅读体验
1. 回顾
如果你没有阅读过 《其实你不知道 Promise.then》 ,可以先阅读这篇文章,在这篇文章中我们提到了很多重要的概念,例如:promiseResolveThenableJobTask,ReslovePromise,接下来的篇幅我们就围绕这些内容展开
2. thenable 接口
在 ECMAScript 暴露的异步结构中,实现了 then() 方法的对象就被认为是 实现了 thenable 接口
const thenable = {
then(resolve, reject) {}
}
// 我们认为这就是一个 thenable 接口
ECMAScript 的 Promise 类实现了 thenable 接口,也就是 《其实你不知道 Promise.then》 这篇文章中所强调的 promise.then 的执行过程中 隐式 处理返回值为 promise 和 非 promise 的意义所在。
我们说 then 方法处理 promise 会在时间顺序上落后处理非 promise 两个感受不到的异步微任务
其中一个微任务是 promiseResolveThenableJobTask,另外一个微任务是 ReslovePromise,这两个微任务有着非常紧密的关系
let promiseResolveThenableJobTask = () => {
p1.then((value) => { ReslovePromise(p2, value) // 传递 promise })
}
只有执行了 promiseResolveThenableJobTask,才会产生 ReslovePromise 这个微任务
微任务 ReslovePromise 的作用是将处理并且将处理后的 promise 传递到外部
大家有没有考虑为什么这里不同步执行
ReslovePromise,而非要创建一个promiseResolveThenableJobTask异步微任务,而这个任务只是一个简单的执行函数的过程
不知道大家有没有见过这种写法:
new Promise(resolve =>
resolve({
then(resolve, reject) {
resolve(1);
},
})
).then(console.log); // 1
这里我们将一个 具有 thenable 接口的对象 传递到 promise 链中,这里面的 cb 是内置的 thenable 接口的参数,我们可以感受到 cb 就是包含了 console.log 的函数 ,所以thenbable 接口也可以作为传递 promise 的参数
让我们来看到题:对比 直接 resolve() 和 resolve(thenable)
const p1 = Promise.resolve({
then(resolve) {
// 这里面的代码是异步的
resolve(1);
},
})
.then(v => console.log(v))
.then(v => console.log(2));
const p2 = Promise.resolve(3)
.then(v => console.log(v))
.then(() => console.log(4));
// 3 1 4 2
resolve 具有 thenable 接口的对象会额外多执行一个微任务,在时间顺序上落后了一个执行 then 的时间,这个多余的微任务作用就是异步执行 cb 传递参数 2,所以 p1 可以等效为
const p1 = Promise.resolve(1)
.then(data => data)
.then(v => console.log(v));
.then(v => console.log(2));
const p2 = Promise.resolve(3)
.then(v => console.log(v))
.then(() => console.log(4));
整个过程就像是两个 promise 链交替输出:空 -> 3 -> 1 -> 4 -> 2
再来看一道更难的题目
Promise.resolve()
.then(() => {
console.log(0);
return {
then(resolve) {
resolve();
},
};
})
.then(() => {
console.log(4);
});
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 4 3 5 6
依然可以将 thenable 接口认为落后一个 then
Promise.resolve()
.then(() => {
console.log(0);
})
.then(data => data)
.then(() => {
console.log(4);
});
输出顺序:0 -> 1 -> 空 -> 2 -> 4 -> 3 -> 5 -> 6
总结:then 方法会根据 return 值的类型开启 不同数量的微任务
- return 非
thenable接口:不落后- return
thenable接口的非promise,落后 1个then的时间- return
promise,落后 2个then的时间
3. promiseResolveThenableJobTask
这个时候你应该或许猜出来了,为什么有上面这个结论:
- return
promise的时候,js引擎为了区分thenable接口是否是Promise实例,如果不是Promise的实例,直接向异步微任务队列中添加了一个ReslovePromise微任务 - return
promise实例的时候首先要调用promiseResolveThenableJobTask,产生出ReslovePromise这个微任务
为什么处理 promise 实例的时候要多此一举呢?
官方的原文解释:
来源:ECMAScript® 2020 Language Specification (ecma-international.org)
翻译如下:此作业使用提供的 thenable 及其方法来解决给定的承诺。此过程必须作为 Job 进行,以确保在完成对任何周围代码的评估后对 then 方法进行评估。
这里的 job / 作业 意思为抽象闭包,也就是多用了一层函数包裹,我能想到的地方:就像下面的代码表现的一样:
const p = Promise.resolve(4);
p.then = function (resolve, reject) {
resolve(4);
};
const p2 = Promise.resolve().then(() => {
return p;
});
p2.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);
});
// 1 2 4 3 5 6
我们使用在 p2的 then 里 return 了一个 promise,但是这个 promise 的 then 方法被我们修改了,这里对 then 方法就行一次判断 ,只会被当做 thenable 接口,而非直接调用 promise.prototype.then
如果稍微修改下代码
p.then = function (resolve, reject) { // 因为是 thenable 产生一个微任务
// 这里可能会产生副作用
Promise.prototype.then.call(
p,
value => {
resolve(value);
},
reject
);
};
// 0 1 2 3 4 5 6
你会发现 promise.prototype.then 也只能产生一个微任务,如果是使用 Promise 实例直接调 promise.prototype.then,会额外向微任务队列中添加 promiseResolveThenableJobTask 这个微任务,这样做的好处可以让可能产生的副作业延后,也就是随着 promiseResolveThenableJobTask 一起被执行掉
4. 总结
当 Promise 实例直接调用 then 方法并且 return thenable 接口的非 promise 时,在时间的顺序上落后处理 1个 then 的时间
结语
本篇文章关于为什么 promiseResolveThenableJobTask 存在的意义来源于我的 猜测 与 推导 ,几乎没有任何文献能够详细的描述这一块的内容,如果你觉得文章有误,或者你有新的见解,欢迎在评论区分享
往期好文推荐: JS中的巨坑 - 数组 - 掘金 (juejin.cn)