til
  • README
  • Software Development Roles
  • solid
  • README
    • service-worker
  • docker
    • arg
    • 更新docker版本
  • editor
    • vscode
    • Creating a VS Code Theme
  • english
    • words
  • front-end
    • ==
    • ECMAScript
    • IIFE
    • Label
    • basic
    • html.js.css渲染顺序
    • npm-vs-yarn
    • obj-delete-key-value
    • react
    • split-join-and-replace
    • video
    • 前端自检清单
    • 递归及去重
    • css
      • css换肤
      • flex
      • list
      • nth-child和nth-of-type区别
      • padding
      • position
      • 层叠上下文
      • 层叠样式(+)
      • 正方形
      • 语义化标签
    • dom
      • DOCTYPE
      • HEAD
      • 修改document
      • 自定义表单验证
    • electron
      • basic
    • es6
      • basic-type
      • basic
      • prototype-example
      • defineProperty
      • understanding-es6
        • 0.introduction
        • Appendix A: Smaller Changes
        • Appendix B: Understanding ES7
        • Block-Binding
        • Proxies&Reflection
        • class
        • 解构赋值
        • function
        • improved-array
        • iterators&generators
        • modules
        • object
        • promise
        • Map&Set
        • symbol
    • images
      • 前端角度看图片
    • interview_case
      • lexical_scope
      • redux和localstroage存储位置
    • javascript
      • fuck-the-js
      • js-engine-work
      • js原生操作dom
      • what-is-function-program
      • 执行上下文
      • articles
        • JavaScript中使用函数组合
        • JavaScript中的依赖注入
        • JavaScript作用域链中的标识符解析和闭包
        • JavaScript是何如工作的--概述
        • JavaScript深拷贝
        • JavaScript的全局变量是如何工作的
        • js继承常见的误解
        • node12&chrome中7个新的提案功能
        • 你真的懂JavaScript吗
      • date
        • index
      • engines
        • basic
        • JavaScript引擎基础:外形和内联缓存
        • v8中推测性优化的介绍
        • 优化原型
        • 更快的异步功能和promise
      • events
        • baisc
        • 事件冒泡和捕获
        • 定义事件
        • 页面生命周期
      • higher-order-function
        • curry
        • monad
      • module
        • basic
        • main&module
      • objects
        • iterator
        • spread
        • examples
          • iterator
      • performance
        • blocking-css
        • cache
      • prototype
        • Property-Descriptors
        • basic
        • prototype-shadow
      • you-dont-known-js
        • async&performance
          • Chapter 1: Asynchrony: Now & Later
          • Chapter 2: Callbacks
          • Chapter 3: Promises
          • Chapter 4: Generators
        • scope & closures
          • apA
          • apB
          • apC
          • apD
          • chapter1-what-is-scope
          • chapter2-lexical-scope
          • chapter3-function-vs-block-scope
          • chapter4-hoisting
          • chapter5-scope-closure
        • this & object prototypes
          • chapter1-this-or-that
          • chapter2-this-make-sense
          • chapter3-objects
          • chapter4-mixing(up)-class-object
          • chapter5-prototype
          • chapter6-behavior-delegation
        • types&grammer
          • Chapter1-Types
          • Chapter2-Values
          • Chapter3-Natives
          • Chapter4-coercion
          • Chapter5-grammer
        • up & going
          • chapter1-into-programming
          • chapter2-into-javascript
          • chapter3-into-YDKJS
    • mobile
      • iPhone分辨率终极指南
    • npm
      • arguments
      • build
    • react-native
      • prop-methods
    • react
      • PropTypes
      • basic
      • codebase-overview
      • component-element-instance
      • context
      • how-to-known-component-is-func-or-class
      • overview
      • react16.9
      • react18计划
      • react的设计原则
      • reconciliation
      • setState
      • useMemo
      • why-do-we-write-super-props
      • 从头实现一个react
      • concurrent
        • 引入并发模式(仅试验)
      • conf
        • conf-2019
      • events
        • 合成事件概述
      • hooks
        • custom-hook
        • effect-hook
        • hooks-api
        • intro
        • overview
        • rules
        • state-hook
        • hooks-vs-class
          • thinking-in-react-hooks
      • overreact
        • Development模式是如何工作的
        • How-Does-setState-Know-What-to-Do
        • Why-Do-React-Elements-Have-a-$$typeof-Property
        • Why-Do-React-Hooks-Rely-on-Call-Order
        • how-to-known-component-is-func-or-class
        • preparing-tach-talk-motivation
        • react作为ui运行
        • things-i-dont-known-as-2018
        • ui-element-problem-and-build-yourself
        • why-do-we-write-super-props
        • 一份完整的useEffect指南
        • 为什么X不是Hook
        • 函数组件与类有什么不同?
        • 演讲准备2-what-why-how
        • 编写弹性组件
        • 让setInterval在React-Hooks中为声明式
      • practice
        • render
      • react-dom
        • basic
      • react-redux
        • apiv7.1-hooks
        • connect
        • shallow-equal
      • redux
        • applyMiddleware
        • applyMiddleware2-细节
        • example
    • regex
      • index
    • stories
      • 数组下标
      • 阻止事件冒泡
    • svelte
      • compile-svelte-in-your-head-1
      • compiler-overview
      • parser
        • 写一个解析器-JavaScript的JSON解析器
    • turbopack
      • basic
    • typescript
      • interface和type的区别
    • webpack
      • hash
      • webpack4-for-react
      • webpack4
      • webpack4to5
      • babel
        • babel-parser和acorn的区别
        • babel.7.11
        • family
        • react16.14使用new-transform
        • update-to-7
    • pdf
      • deep-js
        • basic
      • react
        • reintroducing
  • git
    • capital
    • emoji
  • http
    • http2.0
    • response
  • rails
    • api
    • flash
    • middleware-vs-metal
    • model
    • performance
    • routes
    • environment
      • error
    • patterns
      • service
    • sidekiq
      • params
    • deploy
      • capistrano
        • ssh
  • ruby
    • self
    • net
      • http请求携带cookie
  • server
    • ss
    • ssh
    • user
    • crawler
      • puppeteer
    • nginx
      • domain-without-80
      • nginx节省带宽
  • sql
    • rails
    • search
Powered by GitBook
On this page
  • 一种新的异步编程方法
  • 从回调到promis到异步功能
  • 从事件侦听器回调到异步迭代
  • 异步性能改进
  • (宏)任务与微任务
  • 异步功能
  • 在引擎盖下的await
  • 改善开发者体验
  • 结论
  • 译者结论
  1. front-end
  2. javascript
  3. engines

更快的异步功能和promise

Previous优化原型Nextevents

Last updated 6 years ago

JavaScript中的异步处理传统上以速度不快而闻名。更糟的是,调试实时JavaScript应用程序(特别是Node.js服务器)不是一件容易的事情,尤其是在异步编程方面。幸运的是,时代正在改变。本文探讨了如何在V8中优化异步函数和promis(在某种程度上,在其他JavaScript引擎中也是如此),并描述了如何改进异步代码的调试体验。

注意: 如果你更喜欢观看演示文稿而不是阅读文章,那么请欣赏下面的视频!如果不是,请跳过视频并继续阅读。

一种新的异步编程方法

从回调到promis到异步功能

在promises成为JavaScript语言的一部分之前,基于回调的API通常用于异步代码,尤其是在Node.js中。这是一个例子:

function handler(done) {
  validateParams(error => {
    if (error) return done(error);
    dbQuery((error, dbResults) => {
      if (error) return done(error);
      serviceCall(dbResults, (error, serviceResults) => {
        console.log(result);
        done(error, serviceResults);
      });
    });
  });
}

以这种方式使用深度嵌套回调的特定模式通常被称为“回调地狱” ,因为这会降低代码的可读性并且难以维护。

幸运的是,现在promises已经成为JavaScript语言的一部分,同样的代码可以用更优雅和更易于维护的方式编写:

function handler() {
  return validateParams()
    .then(dbQuery)
    .then(serviceCall)
    .then(result => {
      console.log(result);
      return result;
    });
}
async function handler() {
  await validateParams();
  const dbResults = await dbQuery();
  const results = await serviceCall(dbResults);
  console.log(results);
  return results;
}

使用异步函数,代码变得更简洁,控制和数据流也更容易理解,尽管执行仍然是异步的。(注意,JavaScript执行仍然发生在一个线程中,这意味着异步函数最终不会自己创建物理线程。)

从事件侦听器回调到异步迭代

const http = require("http");

http
  .createServer((req, res) => {
    let body = "";
    req.setEncoding("utf8");
    req.on("data", chunk => {
      body += chunk;
    });
    req.on("end", () => {
      res.write(body);
      res.end();
    });
  })
  .listen(1337);

这段代码可能有点难理解: 传入的数据以块的形式处理,块只能在回调中访问,流结束信号也在回调中发生。当你没有意识到函数立即终止,而实际的处理必须在回调中进行时,很容易在这里引入bug。

const http = require("http");

http
  .createServer(async (req, res) => {
    try {
      let body = "";
      req.setEncoding("utf8");
      for await (const chunk of req) {
        body += chunk;
      }
      res.write(body);
      res.end();
    } catch {
      res.statusCode = 500;
      res.end();
    }
  })
  .listen(1337);

现在,我们可以将所有内容放入单个异步函数中,并使用新的for await…of循环异步迭代块,而不是将实际请求处理的逻辑放入两个不同的回调('data'和'end'回调)中。我们还添加了一个try-catch块来避免unhandledRejection问题[1]。

你现在可以在生产中使用这些新功能!从Node.js 8 (V8 v6.2 / Chrome 62)开始完全支持 异步函数,从Node.js 10 (V8 v6.8 / Chrome 68)开始完全支持 异步迭代器和生成器!

异步性能改进

我们已经成功地在V8的v5.5(Chrome 55和Node.js 7)和V8的v6.8(Chrome 68和Node.js 10)之间显著提高了异步代码的性能。我们达到了一定的性能水平,开发人员可以安全地使用这些新的编程范例,而无需担心速度。

上面的图表显示了一些流行的HTTP中间件框架的性能,这些框架大量使用了promises和async函数。请注意,此图表显示请求数/秒,因此与之前的图表 不同 ,越高的越好。这些框架的性能在Node.js 7(V8 v5.5)和Node.js 10(V8 v6.8)之间得到了显著改善。

这些性能的改善是下列三项主要成就的结果:

  1. node.js 8错误导致await跳过microticks🐛

我们还开发了一个名为Orinoco的新垃圾收集器,它将垃圾收集工作移出主线程,从而显著地改进请求处理。

最后,但也挺重要的是,Node.js 8中有一个便利的bug,在某些情况下会导致await跳过microticks,从而获得更好的性能。这个bug一开始是无意中违反了规范,但是后来给了我们一个优化的想法。让我们从解释错误行为开始:

const p = Promise.resolve();

(async () => {
  await p;
  console.log("after:await");
})();

p.then(() => console.log("tick:a")).then(() => console.log("tick:b"));

上面的程序创建了一个完成(fulfilled)的promise p,并await其结果,但也将两个处理程序链接到它上面。你希望console.log调用以哪种顺序执行?

由于p已经完成(fulfilled),你可能希望它首先打印'after: await'然后打'tick'。实际上,这是你在Node.js 8中得到的行为:

虽然这种行为看起来很直观,但根据规范它并不正确。Node.js 10实现了正确的行为,即首先执行链式处理程序,然后才继续使用异步(async)函数。

这种“正确的行为”可以说并不是很明显,对JavaScript开发人员来说实际上是令人惊讶的,所以有必要解释一下。在深入到promise和异步(async)函数的神奇世界之前,让我们先从一些基础开始。

(宏)任务与微任务

异步功能

根据MDN,异步(async)函数是一个使用隐式promise来异步操作以返回其结果的函数。异步函数的目的是使异步代码看起来像同步代码,从而向开发人员隐藏异步处理的一些复杂性。

最简单的异步函数如下所示:

async function computeAnswer() {
  return 42;
}

当被调用时,它返回一个promise,你可以像任何其他的promise那样获得它的值。

const p = computeAnswer();
// → Promise

p.then(console.log);
// prints 42 on the next turn

你只能在下一次运行微任务时获得这个promise p的值。换句话说,上面的程序在语义上等同于使用带有值的Promise.resolve:

function computeAnswer() {
  return Promise.resolve(42);
}

异步函数的真正强大之处在于await表达式,它导致函数执行暂停,直到一个promise被解决(resolved),并在实现之后恢复,继续执行下去。await的值是完成(fulfilled) promise的值。下面是一个例子:

async function fetchStatus(url) {
  const response = await fetch(url);
  return response.status;
}

fetchStatus的执行在await期间暂停,然后在fetch promise完成(fulfill)时恢复,继续执行下去。这或多或少相当于将处理程序链接到fetch返回的promise上。

function fetchStatus(url) {
  return fetch(url).then(response => response.status);
}

该处理程序包含async函数中await后面的代码。

通常,你会传递一个Promise去await,但实际上你可以await任意JavaScript值。如果await之后的表达式的值不是promise,则将其转换为promise。这意味着如果你愿意,可以await 42:

async function foo() {
  const v = await 42;
  return v;
}

const p = foo();
// → Promise

p.then(console.log);
// prints `42` eventually
class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(() => resolve(Date.now() - startTime), this.timeout);
  }
}

(async () => {
  const actualTime = await new Sleep(1000);
  console.log(actualTime);
})();
async function foo(v) {
  const w = await v;
  return w;
}

调用时,它将参数v包装到promise中并暂停执行异步函数,直到解析该promise。一旦发生这种情况,函数的执行将继续,w将获得已实现(fulfilled) promise 的值。然后从异步函数返回此值。

在引擎盖下的await

首先,V8将这个函数标记为 可恢复 的(resumable),这意味着可以暂停执行,然后恢复执行(await的点)。然后它创建了所谓的implicit_promise,当调用async函数时返回这个promise,并最终解析为async函数生成的值。

接下来是有趣的部分: 实际的await。首先,传递给await的值被包装到一个promise中。然后,处理程序被附加到这个包装的promise上,以在promise完成(fulfilled)后恢复函数,并暂停异步函数的执行,将implicit_promise返回给调用者。一旦履行(fulfilled)了promise,就会使用promise中的值w恢复异步函数的执行,并使用w解析(resolve) implicit_promise。

简而言之,await v的初始步骤是:

  1. 将v(传递给await的值)包装成一个promise。

  2. 附加用于稍后恢复异步函数的处理程序。

  3. 暂停异步函数并将implicit_promise返回给调用者。

执行在调用者中继续,最终调用堆栈变空。然后JavaScript引擎开始运行微任务:它运行先前调度的PromiseResolveThenableJob,它调度新的PromiseReactionJob以将promise链接到传递给await的值。然后,引擎返回到处理微任务队列,因为在继续主事件循环之前必须清空微任务队列。

总结我们所学到的,对于每个await,引擎必须创建 两个额外 的promise(即使右边已经是一个promise),并且它需要 至少三个 微任务队列。谁知道一个简单的await会导致这么多开销 ?!

我们来看看这个开销来自何处。第一行负责创建包装 promise。第二行立即用包装的promise解析(resolve)await的值v。这两行代码由一个额外的promise加上三个microticks中的两个来负责。如果v已经是一个promise(这是常见的情况,因为应用程序通常await promise),这是非常昂贵的。在不太可能的情况下,开发人员await例如42这样的非promise,引擎仍然需要将它包装成一个promise。

以下是新的和改进后的await如何在幕后一步一步地工作:

如果传递给await的值已经是一个promise,那么这种优化避免了创建包装promise的需要,在这种情况下,我们从最少 三个 microticks到 一个 microtick。这种行为类似于Node.js 8所做的,除了现在它不再是一个bug - 它现在是一个正在标准化的优化!

尽管引擎是完全内部的,但是引擎必须创建这种throwaway promise的感觉仍然是错误的。事实证明,throwaway promise只是为了满足规范中内部performPromiseThen操作的API约束。

将Node.js 10中的await与Node.js 12中的优化await进行比较,可以看出这种变化对性能的影响:

async/await现在比手写的promise代码执行得性能更好。 这里的关键点是我们通过修补规范[3]显着减少了异步函数的开销 - 不仅在V8中,而且在所有JavaScript引擎中。

改善开发者体验

在本地开发期间,这是一个非常有用的特性。但是,一旦部署了应用程序,这种方法并不能真正帮助你。在事后调试期间,你只会在日志文件中看到Error#stack输出,而这并不能告诉你关于异步部分的任何信息。

async function foo() {
  await bar();
  return 42;
}

async function bar() {
  await Promise.resolve();
  throw new Error("BEEP BEEP");
}

foo().catch(error => console.log(error.stack));

在Node.js 8或Node.js 10中运行此代码会产生以下输出:

$ node index.js
Error: BEEP BEEP
    at bar (index.js:8:9)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)

请注意,尽管对foo()的调用会导致错误,但foo根本不是堆栈跟踪的一部分。这使得JavaScript开发人员很难执行事后调试,这与代码是部署在web应用程序中还是部署在某个云容器中无关。

这里有趣的是,引擎知道在完成bar时它必须继续的位置:函数foo中的await之后。巧合的是,这也是函数foo被暂停的地方。引擎可以使用这些信息来重构异步堆栈跟踪的部分,即await站点。通过此更改,输出变为:

$ node --async-stack-traces index.js
Error: BEEP BEEP
    at bar (index.js:8:9)
    at process._tickCallback (internal/process/next_tick.js:68:7)
    at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
    at startup (internal/bootstrap/node.js:266:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
    at async foo (index.js:2:3)

在堆栈跟踪中,最前面的函数首先出现,然后是同步堆栈跟踪的其余部分,然后是函数foo中对bar的异步调用。在V8中,这个更改是在新的--async-stack-trace标志之后实现的。

结论

由于两个重要的优化,我们使异步函数更快:

  • 删除两个额外的microticks

  • 取消throwaway promise。

我们还为JavaScript开发人员提供了一些很好的性能建议:

  • 偏向于async函数和await,而不是手写promise代码

  • 坚持JavaScript引擎提供的原生promise实现,以从快捷方式中获益,即为await避免两个microticks。

译者结论

async得到了性能的提升,这是因为nodejs 8中的一个bug,由于这个bug使得v8开发人员得到了感悟,减少不必要的microtick。

await的值如果不是一个promise会被包装一个promise。这个是有问题的,因为大多数的时候,都是await promise,所以有这么一个改进:

可以看到图中,如果是一个promise就直接返回了,否则就包装下。

最近,JavaScript获得了对的支持。现在可以用与同步代码非常相似的方式编写上述异步代码:

另一个在Node.js中特别常见的异步范例是。这是一个例子:

幸运的是,一个名为的很酷的ES2018新特性可以简化这段代码:

[1] => 感谢指出我们。

上面的图表显示了,它测量了大量promise代码的性能。请注意,图表可视化执行时间,意味着更低更好。

的结果更加令人兴奋,它特别强调的性能:

我们设法将Promise.all的性能提高了 8 倍。但是,上面的基准测试是合成的微型基准测试。V8团队更感兴趣的是我们的优化如何影响。

新的优化编译器🎉

新的垃圾回收器🚛

当我们在 8中时,它在各个方面都带来了巨大的性能提升。

在高层次上,JavaScript中有(宏)任务和微任务之分。任务处理I/O和计时器等事件,并一次执行一个。微任务为async/await和promise实现延迟执行,并在每个任务结束时执行。在执行返回到事件循环之前,总是会清空微任务队列。(译:具体的可以查看)

有关更多详细信息,请查看Jake Archibald对的解释。Node.js中的任务模型与之非常相似。

更有趣的是,await适用于任何,即任何带有then方法的对象,即使它不是真正的promise。(译:关于thenable,可以查看)。所以你可以实现一些有趣的事情,比如测量实际sleeping时间的异步sleep:

按照,让我们看看V8为await在引擎下做了什么。这是一个简单的异步函数foo:

让我们一步一步地完成各个操作。假设正在await的东西已经是一个promise,它已经完成(fulfilled)了,并且值是42。然后,引擎创建一个新的promise,并解决任何await的东西。这将在下一个回合中延迟这些承诺的链接,通过规范所称的来表示。

然后引擎创造了另一个所谓的throwaway promise。它被称为 一次性(throwaway),因为没有任何东西被链接到它 - 它完全是引擎内部的。然后将这个throwaway promise链接到promise上,使用适当的处理程序来恢复异步功能。这个performPromiseThen操作基本上是在幕后所做的。最后,暂停执行异步功能,并且控制权返回给调用者。

接下来是,它完成了我们正在await的承诺值的promise - 在这种情况下为42 - 并将反应计划在throwaway promise上。然后引擎再次返回微任务循环,其中包含要处理的最终微任务。

现在,第二个将解析传播到throwaway promise,并恢复异步函数的暂停执行,从await返回值42。

事实证明,规范中已经有一个操作,只在需要时执行包装:

此操作仍然返回promises,并且只在必要时将其他值包装到promises中。这样就可以保存其中一个额外的promise,加上微任务队列上的两个tick,这是传递给await的值已经是promise的常见情况。这个新行为目前是在V8中的--harmony-await-optimization标志后面实现的(从V8 v7.1开始)。我们也; 一旦我们确定它与Web兼容,就应该合并补丁。

让我们再次假设我们在await一个promise,并且完成的时候是42。由于的魔力,现在promise只引用相同的promise v,所以这一步不需要做什么。之后,引擎像之前一样继续运行,创建throwaway promise,调度一个,以便在微任务队列上的下一个tick上恢复异步函数,暂停函数的执行,并返回给调用者。

然后最终当所有JavaScript执行完成时,引擎开始运行微任务,因此它执行。这个任务将promise的解析传播到throwaway,并恢复async函数的执行,从await中产生42。

这一点最近在ECMAScript规范的得到了解决。引擎不再需要创造await的throwaway promise - 大部分时间[2]。

[2] => 如果在Node.js中使用,V8仍然需要创建throwaway承诺,因为before和after钩子是在throwaway promise的上下文中运行的。

[3] => 如前所述,尚未合并到ECMAScript规范中。我们的计划是,一旦我们确定这个改变不会破坏web,我们就会这么做。↩︎

除了性能之外,JavaScript开发人员还关心诊断和修复问题的能力,这在处理异步代码时并不总是那么容易。支持 异步堆栈跟踪 ,即堆栈跟踪不仅包括堆栈的当前同步部分,还包括异步部分:

我们最近一直在研究,它丰富异步函数调用的Error#stack属性。“零成本”听起来令人兴奋,不是吗?当Chrome DevTools功能带来重大开销时,它如何成为零成本?考虑这个示例,其中foo异步调用bar,bar在await promise后抛出异常:

但是,如果将其与上面Chrome DevTools中的异步堆栈跟踪进行比较,你会注意到堆栈跟踪的异步部分中缺少foo的实际调用站点。如前所述,这种方法利用了这样一个事实,即等待恢复和暂停位置是相同的——但是对于常规的Promise#then()或Promise#catch()调用,情况就不是这样了。有关更多背景信息,请参见Mathias Bynens关于为什么的解释。

除此之外,我们还通过改进了开发人员的体验,这些跟踪在async函数和Promise.all()中使用await。

异步功能
ReadableStream
异步迭代(async iteration)
Matteo Collina
这个问题
doxbee基准测试
并行基准测试
Promise.all()
实际用户代码的实际性能
TurboFan
Orinoco
Node.js
发布TurboFan
这里
浏览器中任务,微任务,队列和计划
'thenable'
这里
规范
PromiseResolveThenableJob
Promise.prototype.then()
PromiseReactionJob
PromiseReactionJob
promiseResolve
提出了对ECMAScript规范的这种改变
promiseResolve
PromiseReactionJob
PromiseReactionJob
编辑更改中
async_hooks
补丁
Chrome DevTools
零成本的异步堆栈跟踪
await胜过Promise#then()
零成本的async堆栈跟踪
查看