resolve延迟现象和Promise.resolve真相

杰克-逊の黑豹,恰饭啦 []( ̄▽ ̄)

resolve 延迟现象

在使用 Promise 的时候,我遇到了这样的代码:

ts 复制代码
async function run() {
  return {
    then: (r) => r(111),
  };
}

run().then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

它的输出结果是

txt 复制代码
1
111
2

111 似乎被延迟了一个拍子

我就好奇,它为什么不是

txt 复制代码
111
1
2

还有这个代码

ts 复制代码
async function run() {
  return Promise.resolve(111);
}

run().then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

111 似乎被延迟2个拍子

它输出结果是

txt 复制代码
1
2
111

我就好奇,它为什么不是

txt 复制代码
111
1
2

async 的魔法

我第一个直觉是,问题出自 async,它肯定做了什么不可告人的黑魔法。

看 v8 源码里如何实现 async,显然不太现实,满篇 C++代码,咱看不懂。

不得不吐槽 C++,它是很强,但它就是不想让人懂

然后我就想了一个主意,用 babel 将代码转为 es5, 这样就可以消除 async。

写一个 test.js:

js 复制代码
async function run() {
  return {
    then: (r) => r(111),
  };
}

run().then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

下载 @babel/cli @babel/core @babel/preset-env

shell 复制代码
npm i @babel/cli @babel/core @babel/preset-env

设置好 babel.config.json:

json 复制代码
{
  "presets": ["@babel/preset-env"]
}

把 test.js 编译为 dist.js:

shell 复制代码
npx babel test.js -o dist.js

运行 dist.js:

shell 复制代码
node dist.js

结果是

txt 复制代码
1
111
2

顺序没有变化,这就证明和 async 没有关系;

那到底和什么有关系呢?

在 dist.js 中打断点单步调试,最终发现,和 Promise 有关系!

两个 resolve

这两段代码是等效的:

js 复制代码
async function run() {
  return {
    then: (r) => r(111),
  };
}

run().then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));
js 复制代码
Promise.resolve({ then: (r) => r(111) }).then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

但这两段代码不等效:

js 复制代码
async function run() {
  return Promise.resolve(111);
}

run().then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2))
js 复制代码
Promise.resolve(Promise.resolve(111)).then(console.log);

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

可是改为这样就等效了:

js 复制代码
new Promise((resolve) => resolve(Promise.resolve(111)));

Promise.resolve()
  .then(() => console.log(1))
  .then(() => console.log(2));

问题来了:

  • resolve 函数到底发生了什么

  • Promise.resolve 和 new Promise 里的 resolve 是一个东西么,各自遵从什么规律呢?

resolve 的秘密

问题的答案:

v8 的实现就是按照这个标准写的,facebook 实现的 regenerator 似乎也是根据这个标准来的

先看看 promise-resolve.tq:

tq 复制代码
// https://tc39.es/ecma262/#sec-promise.resolve
transitioning builtin
PromiseResolve(implicit context: Context)(
constructor: JSReceiver, value: JSAny): JSAny {
   // .....一堆代码
}


transitioning builtin
ResolvePromise(implicit context: Context)(
  promise: JSPromise, resolution: JSAny): JSAny {

  // ..... 另一堆代码
}

PromiseResolve ?

ResolvePromise ?

卧槽?

说实话,如果源代码注释里没有写出 ECMAScript 标准的 url,还真不知道这两个函数都是干啥使的。

废话少说,直接上结果:

  • Promise.resolve 就是 PromiseResolve

  • new Promise(resolve => {})resolve 就是 ResolvePromise

先瞧瞧 PromiseResolve:

tq 复制代码
transitioning builtin
PromiseResolve(implicit context: Context)(
  constructor: JSReceiver, value: JSAny): JSAny {

  const nativeContext = LoadNativeContext(context);
  const promiseFun = *NativeContextSlot(
    nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX);

  try {
    // Check if {value} is a JSPromise.
    const value = Cast<JSPromise>(value) otherwise NeedToAllocate;

    // We can skip the "constructor" lookup on {value} if it's [[Prototype]]
    // is the (initial) Promise.prototype and the @@species protector is
    // intact, as that guards the lookup path for "constructor" on
    // JSPromise instances which have the (initial) Promise.prototype.
    const promisePrototype =
      *NativeContextSlot(
        nativeContext, ContextSlot::PROMISE_PROTOTYPE_INDEX);

    // Check that Torque load elimination works.
    static_assert(nativeContext == LoadNativeContext(context));
    if (value.map.prototype != promisePrototype) {
      goto SlowConstructor;
    }

    if (IsPromiseSpeciesProtectorCellInvalid()) goto SlowConstructor;

    // If the {constructor} is the Promise function, we just immediately
    // return the {value} here and don't bother wrapping it into a
    // native Promise.
    if (promiseFun != constructor) goto SlowConstructor;
      return value;
    } label SlowConstructor deferred {
    // At this point, value or/and constructor are not native promises, but
    // they could be of the same subclass.
    const valueConstructor = GetProperty(value, kConstructorString);
    if (valueConstructor != constructor) goto NeedToAllocate;
      return value;
    } label NeedToAllocate {
      if (promiseFun == constructor) {
        // This adds a fast path for native promises that don't need to
        // create NewPromiseCapability.
        const result = NewJSPromise();
        ResolvePromise(context, result, value);
        return result;
    } else deferred {
      const capability = NewPromiseCapability(constructor, True);
      const resolve = UnsafeCast<Callable>(capability.resolve);
      Call(context, resolve, Undefined, value);
      return capability.promise;
    }
  }
}

这代码,废话连篇,简化版就是:

tq 复制代码
transitioning builtin
PromiseResolve(implicit context: Context)(
  constructor: JSReceiver, value: JSAny): JSAny {

  // 以 Promise.resolve(123) 为例,
  // constructor 就是 Promise
  // value 就是 123

  // 根据上下文环境,获取 promiseFun
  const nativeContext = LoadNativeContext(context);
  const promiseFun = *NativeContextSlot(
    nativeContext, ContextSlot::PROMISE_FUNCTION_INDEX);

  // value 是一个 Promise 对象不?
  // 不是,赶紧滚蛋,去执行 NeedToAllocate;
  // 是的话,接着往下执行;
  //
  // Promise.resolve({ then: (r) => r(111) }) 就会执行 NeedToAllocate
  //
  const value = Cast<JSPromise>(value) otherwise NeedToAllocate;

  // promiseFun 是不是 Promise 吧?
  // 不是的话,滚蛋,去执行 SlowConstructor;
  // 是的话,直接返回 value;
  //
  // Promise.resolve(promise) 的结果就是 promise
  //

  if (promiseFun != constructor) goto SlowConstructor;

  return value;

  label NeedToAllocate {
    if (promiseFun == constructor) {

      // 创建一个Promise对象 result,然后返回它;
      // 至于 result 什么时候变成 fulfilled 状态,和 value 之间
      // 有什么关系,就交给 ResolvePromise 搞定;
      //
      // 所以,当传入到 Promise.resolve 的参数不是一个 Promise对象时,
      // new Promise(resolve => resolve(111)) 和 Promise.resolve(111)
      // 是等效的;
      const result = NewJSPromise();
      ResolvePromise(context, result, value);
      return result;
    }
  }
}

读到这里,你应该隐约感到了: new Promise(resolve => {})resolve 怎么去处理接收到的参数,才是决定之前例子中输出结果顺序的关键!

那就看看呗:

tq 复制代码
// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning builtin
ResolvePromise(implicit context: Context)(
  promise: JSPromise, resolution: JSAny): JSAny {
  // 7. If SameValue(resolution, promise) is true, then
  // If promise hook is enabled or the debugger is active, let
  // the runtime handle this operation, which greatly reduces
  // the complexity here and also avoids a couple of back and
  // forth between JavaScript and C++ land.
  // We also let the runtime handle it if promise == resolution.
  // We can use pointer comparison here, since the {promise} is guaranteed
  // to be a JSPromise inside this function and thus is reference comparable.
  if (IsIsolatePromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() ||
    TaggedEqual(promise, resolution))
    deferred {
      return runtime::ResolvePromise(promise, resolution);
  }

  let then: Object = Undefined;
  
  try {
    // 8. If Type(resolution) is not Object, then
    // 8.a Return FulfillPromise(promise, resolution).
    if (TaggedIsSmi(resolution)) {
      return FulfillPromise(promise, resolution);
    }

    const heapResolution = UnsafeCast<HeapObject>(resolution);
    const resolutionMap = heapResolution.map;

    if (!IsJSReceiverMap(resolutionMap)) {
      return FulfillPromise(promise, resolution);
    }

    // We can skip the "then" lookup on {resolution} if its [[Prototype]]
    // is the (initial) Promise.prototype and the Promise#then protector
    // is intact, as that guards the lookup path for the "then" property
    // on JSPromise instances which have the (initial) %PromisePrototype%.
    if (IsForceSlowPath()) {
      goto Slow;
    }

    if (IsPromiseThenProtectorCellInvalid()) {
      goto Slow;
    }

    const nativeContext = LoadNativeContext(context);
    if (!IsJSPromiseMap(resolutionMap)) {
      // We can skip the lookup of "then" if the {resolution} is a (newly
      // created) IterResultObject, as the Promise#then() protector also
      // ensures that the intrinsic %ObjectPrototype% doesn't contain any
      // "then" property. This helps to avoid negative lookups on iterator
      // results from async generators.
      assert(IsJSReceiverMap(resolutionMap));
      assert(!IsPromiseThenProtectorCellInvalid());
      if (resolutionMap ==
        *NativeContextSlot(
          nativeContext, ContextSlot::ITERATOR_RESULT_MAP_INDEX)) {
        return FulfillPromise(promise, resolution);
      } else {
        goto Slow;
      }
    }

    const promisePrototype =
      *NativeContextSlot(
        nativeContext, ContextSlot::PROMISE_PROTOTYPE_INDEX);
    if (resolutionMap.prototype == promisePrototype) {
      // The {resolution} is a native Promise in this case.
      then = *NativeContextSlot(nativeContext, ContextSlot::PROMISE_THEN_INDEX);
      // Check that Torque load elimination works.
      static_assert(nativeContext == LoadNativeContext(context));
      goto Enqueue;
    }

    goto Slow;
  } label Slow deferred {
    // 9. Let then be Get(resolution, "then").
    // 10. If then is an abrupt completion, then
    try {
      then = GetProperty(resolution, kThenString);
    } catch (e) {
      // a. Return RejectPromise(promise, then.[[Value]]).
      return RejectPromise(promise, e, False);
    }

    // 11. Let thenAction be then.[[Value]].
    // 12. If IsCallable(thenAction) is false, then
    if (!Is<Callable>(then)) {
      // a. Return FulfillPromise(promise, resolution).
      return FulfillPromise(promise, resolution);
    }

    goto Enqueue;
  } label Enqueue {
    // 13. Let job be NewPromiseResolveThenableJob(promise, resolution,
    // thenAction).
    const task = NewPromiseResolveThenableJobTask(
      promise, UnsafeCast<JSReceiver>(resolution),
      UnsafeCast<Callable>(then));

    // 14. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
    // 15. Return undefined.
    return EnqueueMicrotask(task.context, task);
  }
}

废话连篇,怎么能忍,上简化版:

tq 复制代码
// https://tc39.es/ecma262/#sec-promise-resolve-functions
transitioning builtin
ResolvePromise(implicit context: Context)(
  promise: JSPromise, resolution: JSAny): JSAny {
  // 以 const t = new Promise(resolve => resolve(123)) 为例,
  // promise 就是 t;
  // resolution 就是 123;

  let then: Object = Undefined;

  // resolution 不是一个 Object, 直接把 promise 设置为
  // fulfilled 状态,值就是 resolution
  if (TaggedIsSmi(resolution)) {
    return FulfillPromise(promise, resolution);
  }

  // 接下来,resolution 肯定是一个对象,但是它可能:
  // 1. 是一个 Promise 对象;
  // 2. 是一个拥有 then 属性的非Promise对象;
  // 得到 Promise.prototype
  const promisePrototype =
    *NativeContextSlot(
      nativeContext, ContextSlot::PROMISE_PROTOTYPE_INDEX);

  // resolution是一个 Promise 对象
  if (resolutionMap.prototype == promisePrototype) {
    // 拿到 Promise.prototype.then
    then = *NativeContextSlot(nativeContext, ContextSlot::PROMISE_THEN_INDEX);
    goto Enqueue;
  }

  goto Slow;

  // resolve 一个 Promise 对象,处理逻辑在 Enqueue;
  // resolve 一个 普通对象(不管它有没有then),处理逻辑在 Slow;

  label Slow deferred {
    try {
      // 获取 resolution.then
      then = GetProperty(resolution, kThenString);
    } catch (e) {
      // GetProperty 出错了,将 promise 设置为 rejected,
      return RejectPromise(promise, e, False);
    }

    // then 可能是 null,可能是 undefined, 可能是 function,也可能是别的Object;
    // 这都没关系,我们只关心它能不能被调用,不能被调用的话,直接将 promise 设置为 fulfilled
    // 状态,值就是 resolution;
    if (!Is<Callable>(then)) {
      return FulfillPromise(promise, resolution);
    }

    // 如果 then 可以被调用,交给逻辑Enqueue处理
    goto Enqueue;

    // 你应该感觉到了,无论resolve一个Promise对象,还是一个含then属性的对象,
    // 最终都要来到 Enqueue, 区别在于,前者的 then 是 Promise.prototype.then,
    // 后者是 resolution.then
  }

  label Enqueue {
    // 13. Let job be NewPromiseResolveThenableJob(promise, resolution,
    // thenAction).
    //
    // 核心逻辑就在 NewPromiseResolveThenableJobTask !
    // 但不好意思,从代码里去理解的话,隔着 c++ 这块臭石头,不方便我们理解,
    // 我们的目的是理解其中的逻辑,不是理解c++语法!
    // 感谢源代码作者留下来的注释吧,到ECMAScript标准里寻找答案吧:
    // https://tc39.es/ecma262/#sec-promise-resolve-functions

    const task = NewPromiseResolveThenableJobTask(
      promise, UnsafeCast<JSReceiver>(resolution),
      UnsafeCast<Callable>(then));

    // 将 task 加入微队列
    return EnqueueMicrotask(task.context, task);
  }
}

标准里规定:

txt 复制代码
NewPromiseResolveThenableJob ( promiseToResolve, thenable, then )

The abstract operation NewPromiseResolveThenableJob takes arguments
promiseToResolve (a Promise), thenable (an Object), and then (a JobCallback
Record) and returns a Record with fields [[Job]] (a Job Abstract Closure) and
[[Realm]] (a Realm Record). It performs the following steps when called:

1. Let job be a new Job Abstract Closure with no parameters that captures
promiseToResolve, thenable, and then and performs the following steps when called:
  a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve).
  b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, << 
  resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] >>)).
  c. If thenCallResult is an abrupt completion, then
    i. Return ? Call(resolvingFunctions.[[Reject]], undefined, << thenCallResult.
    [[Value]] >>).

  d. Return ? thenCallResult.

2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])).

3. If getThenRealmResult is a normal completion, let thenRealm be 
getThenRealmResult.[[Value]].

4. Else, let thenRealm be the current Realm Record.

5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no
code runs, thenRealm is used to create error objects.

6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }.

微队列要执行的就是 job, 只看第一点就可以了,其他的对我们的理解没啥影响。

第一点说的就是,当微队列里的 job 被拿出来调用时,会依次执行 a b c d。

步骤 a,会根据 promiseResolve 创建 resolvingFunctions。

步骤 b, 就是执行 HostCallJobCallback,把它的结果封装在 Completion 里;

步骤 c,就是看看 Completion 是不是正常的 Completion, 对应就是说 promiseToResolve 是不是正常的执行 resolve 了,不是的话,就要将 promiseToResolve 变成 rejected 状态了

步骤 d, 就是一切按照预期正常 resolve 了,就把 Completion 返回就好了;

而核心步骤,是步骤 b。

标准规定:

txt 复制代码
HostCallJobCallback ( jobCallback, V, argumentsList )

1. Assert: IsCallable(jobCallback.[[Callback]]) is true.
2. Return ? Call(jobCallback.[[Callback]], V, argumentsList).

说人话就是:

将函数 jobCallback 的 this 设置为 V, 然后执行 jobCallback(argumentsList)

结合具体情景,我们具体说说这事儿:

js 复制代码
const t = new Promise((resolve, reject) => {
  const a = {
    then: (r) => r(100),
  };

  resolve(a);
});

// 如果是这个情形,
// NewPromiseResolveThenableJob ( promiseToResolve, thenable, then )
// promiseToResolve 就是 t
// thenable 就是 a
// then 就是 a.then
//
// 上述js代码执行后, NewPromiseResolveThenableJob 会产生一个job,加入到
// 微队列中;
//
// 微队列在未来取出job执行,会执行
// HostCallJobCallback(then, thenable, << resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] >>)
//
// 换成我们具体的数据就是
// HostCallJobCallback(a.then, a, resolve, reject)
//
// 再把 HostCallJobCallback 翻译一下就是
// a.then.call(a, resolve, reject)
//
// 一旦执行,会发生什么呢?
// 没错,t 会变成 fulfilled 状态,值是 100 !
//
// 而上述过程,是在一个微任务里完成的,不是上述js代码执行后,立即执行的,因此如果resolve一个定义了then属性
// 的对象,t.then(callbackA),callbackA会延迟一个微任务执行!明面上看,是在定义t的时候,t就resolve了,
// 实际上是在一个微任务里resolve的!
js 复制代码
const t = new Promise((resolve, reject) => {
  const p = Promise.resolve(100);
  resolve(p);
});

// 如果是这个情形,
// NewPromiseResolveThenableJob ( promiseToResolve, thenable, then )
// promiseToResolve 就是 t
// thenable 就是 p
// then 就是 Promise.prototype.then
//
// 上述js代码执行后, NewPromiseResolveThenableJob 会产生一个job,加入到
// 微队列中;
//
// 微队列在未来取出job执行,会执行
// HostCallJobCallback(then, thenable, << resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] >>)
//
// 换成我们具体的数据就是
// HostCallJobCallback(Promise.prototype.then, p, resolve, reject)
//
// 再把 HostCallJobCallback 翻译一下就是
// Promise.prototype.then.call(p, resolve, reject)
//
// 这个函数执行之后,会发生什么呢?
// 没错,在 p 注册了一个 then 回调;
// 根据then回调和微队列的联系,这也意味着 resolve 函数作为一个微任务,加入到微队列了;
// 等到 resolve 作为微任务被执行的时候,就会将 t 设置为 fulfilled 状态,值为 100;
//
// 这种情况下,t 经历了2个微任务变为fulfilled状态,
// 所以 t.then(callbackA), callbackA会延迟两个微任务才执行!

Torque

V8 内部实现了一门叫做 Torque 的语言,用这个语言定义了很多 js 内置对象,目的是提高速度,

Torque 编译之后会得到机器码,减少了 js 和 cpp 之间互调的开销,提高了 js 执行的速度。

你可能在阅读 v8 源码 | promise-resolve.tq 定义 时,看到代码里有 deferred 关键字,以为是延迟执行的意思,想当然以为这个和 resolve

延迟现象有关,很抱歉,我当初就是这么想的,但实际上,deferred关键字只是告诉编译器,被deferred修饰的代码块

被执行的概率很小,可以在编译的时候做优化,和延迟执行没有关系。

snippets

可能你平时都是这么用的:

js 复制代码
const t = Promise.resolve(100);
new Promise((resolve) => {
  t.then((v) => resolve(v));
});

实际上,这也是一样的:

js 复制代码
const t = Promise.resolve(100);
new Promise((resolve) => {
  t.then(resolve);
});

参考

ECMAScript 标准

V8 | Torque language

相关推荐
花花鱼2 分钟前
node-modules-inspector 可视化node_modules
前端·javascript·vue.js
HBR666_5 分钟前
marked库(高效将 Markdown 转换为 HTML 的利器)
前端·markdown
careybobo2 小时前
海康摄像头通过Web插件进行预览播放和控制
前端
TDengine (老段)2 小时前
TDengine 中的关联查询
大数据·javascript·网络·物联网·时序数据库·tdengine·iotdb
杉之3 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
喝拿铁写前端3 小时前
字段聚类,到底有什么用?——从系统混乱到结构认知的第一步
前端
再学一点就睡3 小时前
大文件上传之切片上传以及开发全流程之前端篇
前端·javascript
木木黄木木4 小时前
html5炫酷图片悬停效果实现详解
前端·html·html5
请来次降维打击!!!5 小时前
优选算法系列(5.位运算)
java·前端·c++·算法
難釋懷5 小时前
JavaScript基础-移动端常见特效
开发语言·前端·javascript