罪魁祸首还是先挂出来 ?
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);
})
接上篇 ? 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
手写 Promise 完整代码 ? github:my-promise
问题回顾
上篇我们通过从零手写 Promise 的方式,带着大家去深入了解了一下 Promise 的一些实现细节。
最后我们发现其实只需要创建一次微任务,就可以处理 then 方法内部 return Promise.resolve(4)
的问题,所以我们没有办法在手写 Promise 实现中去找到到合理的解释,只能通过一些概念进行了猜测。
没有真正窥探到原生 Promise 的内部实现逻辑,似乎让人感觉有点隔靴搔痒 ?
虽然一次还是两次微任务对我们实际生产也没有什么实质影响,创建两次微任务的代码逻辑也可能会在后续的某次迭代中被改掉。
但是我们不能不面对新的疑问:
- 原生 Promise 是不是真的产生了两次微任务来处理 return Promise.resolve(4)?
- Promise V8 源码中有没有关键信息可以解释这个现象?
本着刨根问题对精神,还是决定做一下 Promise V8 源码内容补充,也算是对 V8 源码学习的一次启蒙。
Promise V8 源码如何阅读?
废话不多说,来开始看源码 ? 源码地址,注释内容来自我们ECMAScript® 2022 规范。
你可能会看的脑壳疼 ? 更糟糕的是 Promise 的实现逻辑是实际上分布在不同的代码块中的,直接吃生肉很容易消化不良。
所以这里先推荐两篇 Promise V8源码分析文章(PS:我尝过,是熟的):
- Promise V8 源码分析(一)
- Promise V8 源码分析(二)
在大致熟悉 Promise V8 源码的之后,我们再回到之前的问题 ?
原生 Promise 是不是真的产生了两次微任务?
在 Promise V8 源码中通过 RunSingleMicrotask 运行一个微任务。如果想要了解微任务的创建情况,就可以通过在RunSingleMicrotask 打印调用信息来观察。
我们来看一下在运行那道面试题时,RunSingleMicrotask 中打印的信息,图片来自知乎@徐鹏跃
用红框圈出的信息,就是那两次神秘的微任务,所以我们这里我们就可以确认,确实是创建了两次微任务。
Promise V8 源码中关键信息在哪里?
实际上我们通过上一篇的分析,我们知道有一次微任务创建的位置是很清晰的。那就是在发现 onFufilled 回调函数执行结果是一个 Promise 的时候,它会调用一次 then 方法去处理这种情况,调用 then 方法那就必然会使用 queueMicrotask 创建一次微任务。
先看一下面试题中这个 Promise
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then((res) => {
console.log(res)
})
我们再来回顾一下上一篇中是如何处理 return Promise.resolve(4) 的 ?
- Promise.resolve() 执行,修改 Promise 状态为 fulfilled;
// 更改成功后的状态
resolve = (value) => {
// 只有状态是等待,才执行状态修改
if (this.status === PENDING) {
// 状态修改为成功
this.status = FULFILLED;
// 保存成功之后的值
this.value = value;
// resolve里面将所有成功的回调拿出来执行
while (this.onFulfilledCallbacks.length) {
// Array.shift() 取出数组第一个元素,然后()调用,shift不是纯函数,取出后,数组将失去该元素,直到数组为空
this.onFulfilledCallbacks.shift()(value)
}
}
}
- then 初始化的时候,在这之前 Promise.resolve() 已经修改状态为 fulfilled,所以这里会立即通过 queueMicrotask 创建微任务将 onFulfilled 回调函数送入微任务队列;
// onFulfilled 回调函数
onFulfilled = () => {
console.log(0);
return Promise.resolve(4);
}
// 创建一个微任务等待 promise2 完成初始化
queueMicrotask(() => {
try {
// 获取成功回调函数的执行结果
const x = realOnFulfilled(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error)
}
})
- 在 then 全部初始化完成后,同步代码执行结束,开始执行微任务列表中排队的任务,onFulfilled 回调函数此时会被调用,onFulfilled 函数的执行结果 x 会传入 resolvePromise 方法进行处理,此时 x 为 Promise.resolve(4) ;
// 获取成功回调函数的执行结果
const x = realOnFulfilled(this.value);
// 传入 resolvePromise 集中处理
resolvePromise(promise2, x, resolve, reject);
- 判断返回值 x 的类型,如果
typeof x === object
或者typeof x === function
,同时判断x.then
存在,此时 x 为 Promise.resolve(4),符合上面的条件,则调用 then 方法(这里就会创建一次微任务),得到结果 y 继续调用 resolvePromise 递归判断,这里 y = 4,即不为 Promise, 调用resolve(4)
,注意这里的resolve 方法是外部 Promise 的,相当于将 Promise.resolve(4) 的执行状态与结果提供给外部的 Promise,完整代码是这样 ?
function resolvePromise(promise, x, resolve, reject) {
// 如果相等了,说明return的是自己,抛出类型错误并返回
if (promise === x) {
return reject(new TypeError('The promise and the return value are the same'));
}
if (typeof x === 'object' || typeof x === 'function') {
// x 为 null 直接返回,走后面的逻辑会报错
if (x === null) {
return resolve(x);
}
let then;
try {
// 把 x.then 赋值给 then
then = x.then;
} catch (error) {
// 如果取 x.then 的值时抛出错误 error ,则以 error 为据因拒绝 promise
return reject(error);
}
// 如果 then 是函数
if (typeof then === 'function') {
let called = false;
try {
then.call(
x, // this 指向 x
// 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
y => {
// 如果 resolvePromise 和 rejectPromise 均被调用,
// 或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
// 实现这条需要前面加一个变量 called
if (called) return;
called = true;
resolvePromise(promise, y, resolve, reject);
},
// 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
r => {
if (called) return;
called = true;
reject(r);
});
} catch (error) {
// 如果调用 then 方法抛出了异常 error:
// 如果 resolvePromise 或 rejectPromise 已经被调用,直接返回
if (called) return;
// 否则以 error 为据因拒绝 promise
reject(error);
}
} else {
// 如果 then 不是函数,以 x 为参数执行 promise
resolve(x);
}
} else {
// 如果 x 不为对象或者函数,以 x 为参数执行 promise
resolve(x);
}
}
通过对手写 Promise 回顾,我们知道在处理 Promise.resolve(4)的时候,调用了 then 方法,来修改状态并拿到 Promise 的结果,这里也就创建了一次微任务。回过来我们再看一下在原生 Promise 中是怎么处理的。
实际上在 Promise V8 源码中也有类似上面的 resolvePromise 的处理,在 ResolvePromise 方法中 ?
// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning builtin
ResolvePromise(implicit context: Context)(
promise: JSPromise, resolution: JSAny): JSAny {
try {
// 8. If Type(resolution) is not Object, then
// 8.a Return FulfillPromise(promise, resolution).
// 如果 resolution 是整数/字符串
if (TaggedIsSmi(resolution)) {
// FulfillPromise 把 promise 状态变为 fulfilled 状态
return FulfillPromise(promise, resolution);
}
const promisePrototype =
*NativeContextSlot(ContextSlot::PROMISE_PROTOTYPE_INDEX);
// 判断 resolution 的类型是否为 Promise
if (resolutionMap.prototype == promisePrototype) {
// The {resolution} is a native Promise in this case.
then = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
// Check that Torque load elimination works.
static_assert(nativeContext == LoadNativeContext(context));
goto Enqueue;
}
} label Enqueue {
// 13. Let job be NewPromiseResolveThenableJob(promise, resolution,
// 代码逻辑与规范一致,把 NewPromiseResolveThenableJob 送入微任务队列
const task = NewPromiseResolveThenableJobTask(
promise, UnsafeCast<JSReceiver>(resolution),
UnsafeCast<Callable>(then));
// 14. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
// 15. Return undefined.
// 插入 microtask 队列
return EnqueueMicrotask(task.context, task);
}
}
通过 resolutionMap.prototype == promisePrototype
判断是否为 Promise,发现 onFulfilled 执行结果是一个 Promise 的时候,会创建 NewPromiseResolveThenableJob 并插入 microtask 队列中。这里实际上就是与我们手写代码存在差异的地方,也是多出的一次微任务创建的位置。在 ECMAScript® 2022 中也有说明这一块的规范 ?
接着,我们看一下 PromiseResolveThenableJob 里面到底是做了什么 ?
// https://tc39.es/ecma262/#sec-promiseresolvethenablejob
transitioning builtin
PromiseResolveThenableJob(implicit context: Context)(
promiseToResolve: JSPromise, thenable: JSReceiver, then: JSAny): JSAny {
const nativeContext = LoadNativeContext(context);
const promiseThen = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
const thenableMap = thenable.map;
if (TaggedEqual(then, promiseThen) && IsJSPromiseMap(thenableMap) &&
!IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() &&
IsPromiseSpeciesLookupChainIntact(nativeContext, thenableMap)) {
// PerformPromiseThen 方法也是 JS Promise then 方法的底层调用
return PerformPromiseThen(
UnsafeCast<JSPromise>(thenable), UndefinedConstant(),
UndefinedConstant(), promiseToResolve);
} else {
const funcs =
CreatePromiseResolvingFunctions(promiseToResolve, False, nativeContext);
const resolve = funcs.resolve;
const reject = funcs.reject;
try {
return Call(
context, UnsafeCast<Callable>(then), thenable, resolve, reject);
} catch (e) {
return Call(context, UnsafeCast<Callable>(reject), Undefined, e);
}
}
}
PerformPromiseThen 方法实际上也是 Promise then 方法的底层核心方法,在 ECMAScript® 2022 中我们可以看到 ?
我们看一下 Promise then 的 源码 ?
transitioning javascript builtin
PromisePrototypeThen(js-implicit context: NativeContext, receiver: JSAny)(
onFulfilled: JSAny, onRejected: JSAny): JSAny {
// 1. Let promise be the this value.
// 2. If IsPromise(promise) is false, throw a TypeError exception.
const promise = Cast<JSPromise>(receiver) otherwise ThrowTypeError(
MessageTemplate::kIncompatibleMethodReceiver, 'Promise.prototype.then',
receiver);
// 3. Let C be ? SpeciesConstructor(promise, %Promise%).
const promiseFun = UnsafeCast<JSFunction>(
context[NativeContextSlot::PROMISE_FUNCTION_INDEX]);
// 4. Let resultCapability be ? NewPromiseCapability(C).
let resultPromiseOrCapability: JSPromise|PromiseCapability;
let resultPromise: JSAny;
label AllocateAndInit {
const resultJSPromise = NewJSPromise(promise);
resultPromiseOrCapability = resultJSPromise;
resultPromise = resultJSPromise;
}
// onFulfilled 和 onRejected 是 then 接收的两个参数
const onFulfilled = CastOrDefault<Callable>(onFulfilled, Undefined);
const onRejected = CastOrDefault<Callable>(onRejected, Undefined);
// 5. Return PerformPromiseThen(promise, onFulfilled, onRejected,
// resultCapability).
// 这里是上面 ECMAScript 截图中对应的第5点,Return PerformPromiseThen
PerformPromiseThenImpl(
promise, onFulfilled, onRejected, resultPromiseOrCapability);
// 返回一个新的 Promise
return resultPromise;
}
再来看一下 PerformPromiseThen 的源码 ?
// https://tc39.es/ecma262/#sec-performpromisethen
transitioning builtin
PerformPromiseThen(implicit context: Context)(
promise: JSPromise, onFulfilled: Callable|Undefined,
onRejected: Callable|Undefined, resultPromise: JSPromise|Undefined): JSAny {
// 调用 PerformPromiseThenImpl 方法
PerformPromiseThenImpl(promise, onFulfilled, onRejected, resultPromise);
return resultPromise;
}
对比一下,我们发现他们实际上都是调用了 PerformPromiseThenImpl 方法来处理核心逻辑的,我们再看一下 PerformPromiseThenImpl中做了什么 ?
transitioning macro PerformPromiseThenImpl(implicit context: Context)(
promise: JSPromise, onFulfilled: Callable|Undefined,
onRejected: Callable|Undefined,
resultPromiseOrCapability: JSPromise|PromiseCapability|Undefined): void {
if (promise.Status() == PromiseState::kPending) {
// pending 状态的分支
// The {promise} is still in "Pending" state, so we just record a new
// PromiseReaction holding both the onFulfilled and onRejected callbacks.
// Once the {promise} is resolved we decide on the concrete handler to
// push onto the microtask queue.
const handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
// 拿到 Promise 的 reactions_or_result 字段
const promiseReactions =
UnsafeCast<(Zero | PromiseReaction)>(promise.reactions_or_result);
// 考虑一个 Promise 可能会有多个 then 的情况,reaction 是个链表
// 存储 Promise then 中传入的回调函数
const reaction = NewPromiseReaction(
handlerContext, promiseReactions, resultPromiseOrCapability,
onFulfilled, onRejected);
// reactions_or_result 可以存 Promise 的处理函数,也可以存
// Promise 的最终结果,因为现在 Promise 处于 pending 状态,
// 所以存的是处理函数 reaction
promise.reactions_or_result = reaction;
} else {
// fulfilled 和 rejected 状态的分支
const reactionsOrResult = promise.reactions_or_result;
let microtask: PromiseReactionJobTask;
let handlerContext: Context;
if (promise.Status() == PromiseState::kFulfilled) {
handlerContext = ExtractHandlerContext(onFulfilled, onRejected);
microtask = NewPromiseFulfillReactionJobTask(
handlerContext, reactionsOrResult, onFulfilled,
resultPromiseOrCapability);
} else
deferred {
assert(promise.Status() == PromiseState::kRejected);
handlerContext = ExtractHandlerContext(onRejected, onFulfilled);
microtask = NewPromiseRejectReactionJobTask(
handlerContext, reactionsOrResult, onRejected,
resultPromiseOrCapability);
if (!promise.HasHandler()) {
runtime::PromiseRevokeReject(promise);
}
}
// fulfilled 和 rejected 状态时,将 onRejected onFulfilled 放入微任务队列
// 等待执行
EnqueueMicrotask(handlerContext, microtask);
}
promise.SetHasHandler();
}
这里我们再次看到了熟悉的 EnqueueMicrotask(),它的出现意味着又有新的微任务被创建,这个与我们手写 Promise 实现中的处理逻辑基本一致,也就是 then 调用时所创建的那次微任务。
所以这里我们总结一下原生 Promise 创建两次微任务的位置:
- 发现 Promise.resolve(4) 的时候,创建 NewPromiseResolveThenableJob,并将其送入微任务队列(与手写有差异);
- 处理 Promise.resolve(4) 的时候,调用 then 方法时,内部创建了微任务来处理回调函数(与手写类似);
写在最后
首先特别感谢知乎@徐鹏跃 在 Promise V8 源码解析这块提供的支持,为文章提供了很多关键信息。
另外关于这道面试题,我也创建了知乎问题,得到了很多非常棒的回答,也推荐大家去看看 promise.then 中 return Promise.resolve 后,发生了什么?
参考资料:
- zhuanlan.zhihu.com/p/264944183
- zhuanlan.zhihu.com/p/329201628
- tc39.es/ecma262/
- chromium.googlesource.com/v8/v8.git/+…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!