• v8是怎么实现更快的 await ?深入理解 await 的运行机制


     

    最近v8团队发表一篇博客Faster async functions and promises, 预计在v7.2版本实现更快的异步函数和promise

    文章内容看起来不是很容易理解,背后的原理比较隐蔽,不过博客提到的一些ECMAScript 标准文档中的操作、任务,实际上都有已经实现的 built-in api, 因此我们可以借助我们比较熟悉的语法、api 来理解其中的原理,

    也许本文有些说法不够准确,欢迎纠正

    Example

    首先看下博客开篇提到的代码:

    const p = Promise.resolve();
    
    (async () => {
      await p;
      console.log("after:await");
    })();
    
    p.then(() => {
      console.log("tick:a");
    }).then(() => {
      console.log("tick:b");
    });
    
    以 node v10 的执行结果为准,node v8 的实现是不符合ECMAScript 标准

    优秀的程序员总是能以简单的例子解释复杂的原理。代码很简单,但是执行结果可能出乎很多人意料:

    tick:a
    tick:b
    after:await
    
    如果你已经猜对了,本文的关键内容你已经掌握,不用往下看了:)。

    为什么 after:await会出现在tick:a之后,甚至是tick:b之后? 要理解其中的原理,我们可以做一个小实验。

    将 await 翻译成 promise

    v8博客中是以伪代码的方式解释await的执行逻辑: 

    原图 https://v8.dev/_img/fast-async/await-under-the-hood.svg

    我们可以用promise语法写成:

    function foo2(v) {
      const implicit_promise = new Promise(resolve => {
        const promise = new Promise(res => res(v));
        promise.then(w => resolve(w));
      });
    
      return implicit_promise;
    }
    

    按照同样的方式,可以将文章开头的代码转换成:

    const p = Promise.resolve();
    
    (() => {
      const implicit_promise = new Promise(resolve => {
        const promise = new Promise(res => res(p));
        promise.then(() => {
          console.log("after:await");
          resolve();
        });
      });
    
      return implicit_promise;
    })();
    
    p.then(() => {
      console.log("tick:a");
    }).then(() => {
      console.log("tick:b");
    });
    

    经过一些琐碎的调试,发现问题真正的关键代码是这一句: const promise = new Promise(res => res(p));

    Resolved with another promise

    了解 Node.js 或浏览器的事件循环的童鞋都知道,resolved promise 的回调函数(reaction)是放在一个单独的队列MicroTask Queue中。 这个队列会在事件循环的阶段结束的时候被执行,只有当这个队列被清空后,才能进入事件循环的下一个阶段。

    我们知道一个 promise 的 .then 回调的返回值可以是一个任意值,也可以是另外一个 promise。 但是后者的处理逻辑可能有点反直觉

    在深入之前,我们简单说一下 promise 的几种状态:

    • 我们说一个 promise 是 resolved 的,表示它不能被再次 fulfill 或 reject, 要么是被 fulfill,要么被 reject(这两种情况,promise 均有一个确定的 non-promise result), 要么遵循另外一个 promise(随之 fulfill 或 reject)
    • 我们说一个 promise 是 unresolved 的,表示它尚未被 resolve

    当一个 promise(假设叫 promiseA。方便引用) 被 resolve,并且去遵循另外一个 promise(叫 p) 时,执行逻辑和前面两种 resolve 情况非常不同,用伪代码表示则是:

    addToMicroTaskQueue(() => { // 任务A
      // 使用 .then 方法,将 promiseA 的状态 和 p 绑定
      p.then(
        resolvePromiseA, // 任务B
        rejectPromiseA
      );
    });
    

    我们一步一步来分析:

    1. 首先,我们在MicroTask Queue添加任务A,该任务在 ECMAScript 标准 中被定义为 PromiseResolveThenableJob
    2. 任务A,主要目的是使 promiseA 遵循 p 的状态,将两者的状态关联起来。 
    3. 由于我们例子中 p 已经是 resolved(状态为fulfilled)的,所以立即将resolvePromiseA任务B 添加到MicroTask Queue
    4. 在 resolvePromiseA 执行后,promiseA 才是 resolved (状态为 fulfilled,值为 p 的 fulfilled value)

    我们可以看到,从 new Promise(res=>res(p)) 到该调用返回的 promise 真正被 resolve 至少需要两次microtick——在我们的例子中,是遍历了两次 MicroTask Queue

    这个时候,我们终于可以理清楚开头代码的执行顺序:

    1、当代码执行完后

      • MicroTask Queue有两个任务:tick:aPromiseResolveThenableJob

    2、开始执行 runMicrotasks()

      • MicroTask Queue变成:tick:bresolvePromiseA
      • console: tick:a

    3、MicroTask Queue没有清空,继续执行队列中的任务

      • MicroTask Queue变成:after:await
      • console: tick:a, tick:b

    4、继续执行,清空MicroTaak Queue

      • console: tick:a, tick:b, after:await

    未来更快的 v8

    借助我们更熟悉的promise,我们基本知道了现阶段的await的执行机制,这样我们就能很好理解为什么 v8 博客中提到的改进可以使 await 执行更快:

    将 new Promise(res=>res(p)) 替换成 Promise.resolve(p)

    根据MDN文档, 当 p 是一个 promise 时,Promise.resolve(p)直接返回 p,而这是大概率事件。

    因此,我们减少了 promise 之间状态同步需要的两次 microtick,那样,上述代码的输出结果就是:

    after:await
    tick:a
    tick:b
    
    编辑于 2019-01-04
  • 相关阅读:
    springboot + ApplicationListener
    spring-boot集成swagger
    Servlet对象生命周期(四)
    MyEclipse 基本使用(三)
    Servlet视频-开发第一个java web(最简单的java web程序)(二)
    java Servlet学习笔记(一)
    java JDBC
    冒泡和选择排序 事例
    c#转 java学习笔记(原创)
    数据存储、进制转换
  • 原文地址:https://www.cnblogs.com/williamjie/p/10233415.html
Copyright © 2020-2023  润新知