codercao

JavaScript是如何工作的:事件循环和异步编程的兴起-5个关于如何使用async/await编写更简洁代码的技巧

原文链接: blog.sessionstack.com

#JavaScript是如何工作的:事件循环和异步编程的兴起-5个关于如何使用async/await编写更简洁代码的技巧

欢迎来到本系列的第4部分,专门讨论JavaScript及其构建组件。在识别和描述核心元素的过程中,我们还分享了构建SessionStack时使用的一些经验法则,这个JavaScript应用程序必须保持强大和高性能,才能保持竞争力。

你错过了前三章吗?你可以在这里找到它们:

  1. 引擎、运行时和调用堆栈的概述

  2. 在Google的V8引擎和5个关于如何编写优化代码的建议

  3. 内存管理+如何处理4个常见的内存泄漏

这一次,我们将通过回顾单线程环境中编程的缺点,以及如何克服它们来构建令人惊叹的JavaScript ui,来扩展我们的第一篇文章。按照惯例,在本文的最后,我们将分享5个关于如何使用async/await编写更简洁代码的技巧。

为什么有一个线程是一个限制?

在我们发布的 第一篇文章中,我们思考了一个问题,当调用堆栈中的函数调用需要花费大量的时间来处理时,会发生什么。

例如,想象一个在浏览器中运行的复杂的图像转换算法:

虽然调用堆栈有执行的功能,但是浏览器不能做其他任何事情—它被阻塞了。这意味着浏览器不能呈现,它不能运行任何其他的代码,它只是被卡住了。问题是,你的应用的界面已经不再高效和美观了。

你的应用 被卡住了.

有时候,这可能不是一个严重的问题。但是,它将会带来更大的问题。一旦你的浏览器开始在调用栈中处理太多的任务,它可能会在很长一段时间内停止响应。在这一点上,许多浏览器会通过增加错误来采取行动,询问是否应该终止页面:

它很丑,它完全毁了你的用户体验:

JavaScript程序的构建块

也许你只是写一个JavaScript方法在一个js文件,但是您的程序几乎肯定是由几个块组成的,其中只有一个将要执行,其余的将在以后执行。最常用的块单元是函数。 大多数开发人员对JavaScript的新问题似乎是理解,以后的事情并不一定会立即发生。换句话说,现在无法完成的任务,从定义上来说,是异步完成的,这意味着你不会像你在潜意识中预料到的那样,就会有上述的阻塞行为

让我们来看看下面的例子:

您可能已经意识到标准的Ajax请求不会同步完成,这意味着在代码执行的时候Ajax(..)函数还没有返回到响应变量的任何值 一个简单的“waiting”异步函数返回其结果的方法是使用一个名为回调的函数

请注意:您实际上可以实现同步Ajax请求。永远不要这样做。如果你进行同步Ajax请求,你的JavaScript应用程序的UI界面将会被阻塞——用户将无法单击、输入数据、导航或滚动。这将防止任何用户交互。这是一种糟糕的做法。

这就是它的样子,但请千万不要这样做——不要破坏网络:

我们使用了Ajax请求作为示例。你可以让任意块代码异步执行。

这是可以做到的 setTimeout(callback, milliseconds) 方法. setTimeout函数所做的是设置一个事件(超时),以便稍后发生。让我们来看看:

控制台的输出如下:

first
third
second

解析事件循环

我们将从一个临时的声明开始—尽管允许异步JavaScript代码(如我们刚刚讨论的setTimeout),直到ES6,JavaScript本身实际上从未有任何关于异步的直接概念。JavaScript引擎在任何给定的时刻都没有执行过任何一项程序。

有关JavaScript引擎如何工作的更多细节(特别是Google的V8引擎),请检查我们 前面的文章

那么,谁告诉JS引擎来执行你的程序的块呢?实际上,JS引擎并不是单独运行的——它运行在一个托管环境中,对于大多数开发人员来说,这是典型的web浏览器或node.JS。实际上,如今,JavaScript已经嵌入到各种设备中,从机器人到电灯泡。每一个设备代表了JS引擎的不同类型的托管环境。

在所有环境中,共同的分母是一个称为事件循环 事件循环的内置机制,它可以在每次调用JS引擎时,处理程序的多个块的执行。

这意味着,JS引擎只是针对任意JS代码的按需执行环境。它是围绕事件(JS代码执行)的周围环境。

所以,例如当您的JavaScript程序发出Ajax请求从服务器获取一些数据时,你在一个函数中设置了“response”代码(“callback”),而JS引擎告诉托管环境:

“嘿,我现在要暂停执行了,但是无论何时你完成了这个网络请求,你有了一些数据,请把这个函数重新调用。”

然后设置浏览器以侦听来自网络的响应,当它有什么东西返回给你的时候,它将通过将它插入到事件循环中来调度回调函数。

让我们看下下图:

你可以在 前一篇文章中了解更多关于内存堆和调用堆栈的信息

这些Web API是什么? 本质上,它们是您无法访问的线程,你可以对它们进行调用。它们是并行启动的浏览器的一部分。如果你是一个 Node.jS开发者,这些是C++API。

那么,事件循环到底是什么呢?

事件循环有一个简单的工作——监视调用堆栈和回调队列。如果调用堆栈是空的,它将从队列中取出第一个事件,并将其推到调用堆栈,该堆栈有效地运行它。 这样的迭代被称为事件循环中的 一个标记 。每个事件都只是一个函数回调。

让我们“execute”这段代码,看看会发生什么:

  1. 状态是很清晰的. 浏览器控制台显示,调用堆栈是空的.

2. console.log('Hi') 被添加到调用堆栈中.

3. console.log('Hi') 执行.

4. console.log('Hi') 从调用堆栈中删除.

5. setTimeout(function cb1() { ... }) 被添加到调用堆栈中.

6. setTimeout(function cb1() { ... }) 执行。浏览器将作为Web api的一部分创建一个计时器。它将为你处理倒计时。

7. The setTimeout(function cb1() { ... }) 它本身是完整的,并且从调用堆栈中删除。

8. console.log('Bye') 被添加到调用堆栈中.

9. console.log('Bye') 执行.

10. console.log('Bye')从调用堆栈中删除.

11. 在至少 5000 ms 后, 计时器完成并将cb1回调推到回调队列。

12. 事件循环从回调队列中获取cb1,并将其推到调用堆栈。

13. cb1 执行 和添加 console.log('cb1')到调用堆栈.

14. console.log('cb1') 执行.

15. console.log('cb1') 从调用堆栈中删除.

16. cb1 从调用堆栈中删除.

一个快速回顾:

值得注意的是,ES6指定了事件循环应该如何工作,这意味着从技术上讲,它在JS引擎的职责范围内,不再只是扮演宿主环境的角色。造成这种变化的一个主要原因是在ES6中引入了Promises,因为后者需要对事件循环队列的调度操作进行直接的、细粒度的控制(稍后我们将详细讨论它们)。

setTimeout(…) 是怎么样工作的

需要注意的是setTimeout(…)不会自动将您的回调放到事件循环队列中。它设置了一个计时器。当计时器过期时,环境将您的回调放入事件循环中,以便将来的某个时间点将会接收并执行它。看看这段代码:

这并不意味着myCallback 将在1000毫秒内执行,而是在1000毫秒内,myCallback将被添加到队列中。不过,队列可能会有其他之前添加的事件——你的回调将不得不等待。 有相当多的文章和教程上开始使用异步JavaScript代码,建议做setTimeout(callback, 0),现在你知道什么事件循环和setTimeout是如何工作的:调用setTimeout 0作为第二个参数只是推迟回调,直到调用堆栈明确。

看看下面的代码:

尽管等待时间设置为0毫秒,但浏览器控制台的结果如下:

Hi
Bye
callback

作业在ES6是怎么样的 ?

在ES6中引入了一个称为“作业队列”的新概念。它是事件循环队列顶部的一个层。在处理承诺的异步行为时,您很可能会遇到这种情况(我们也会讨论它们)。 我们现在只讨论这个概念,这样当我们讨论异步行为时,就会明白这些行为是如何被调度和处理的。

想象一下这样的情况:作业队列是一个队列,它连接到事件循环队列的每一个标记的末尾。在事件循环期间可能发生的某些异步操作不会导致将整个新事件添加到事件循环队列中,而是会将一个项目(也就是Job)添加到当前滴答响的作业队列的末尾。

这意味着你可以添加稍后执行的另一个功能你可以放心,它将在其他任何事情之前执行。

作业还可以在同一队列的末尾添加更多的作业。从理论上讲,一份工作“循环”(一份不断增加其他工作的工作,等等)可能会无限期地旋转,这样就会使程序缺乏必要的资源,从而转移到下一个事件循环。在概念上,这类似于表示一个长时间运行的或无限循环(就像while (true))在你的代码中。

作业有点像 setTimeout(callback, 0) ,“hack”,但是实现了这样一种方式,他们引入了一个更明确、更有保证的顺序:稍后,但是尽可能快。

回调

正如你已经知道的,回调是目前为止在JavaScript程序中表达和管理异步的最常用的方式。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂和复杂的程序,都是在没有其他异步基础之上编写的,而不是回调。

除了回调不会有任何缺点。许多开发人员都在尝试寻找更好的异步模式。但是,如果你不理解底层的实际情况,就不可能有效地使用任何抽象。

在接下来的章节中,我们将深入探讨这些抽象概念,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是推荐的。

嵌套回调

看看下面这段代码

我们已经将三个函数的链嵌套在一起,每一个都表示异步序列中的一个步骤。

这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与缩进/缩进几乎没有任何关系。这是一个更深层次的问题。

首先,我们在等待“click”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次出现。

乍一看,这段代码似乎可以自然地将它的异步映射为连续的步骤:

然后我们有:

然后我们有:

最后:

因此,这样一种顺序的方式来表达您的异步代码似乎更自然,不是吗?一定有这样的方法,对吧?

Promises

看看下面这段代码:

它非常简单:它对x和y的值进行求和,并将其打印到控制台。但是,如果xy的值缺失了,还有待确定呢?例如,我们需要从服务器检索x和y的值,然后才能在表达式中使用它们。让我们假设我们有一个函数loadXloadY ,分别从服务器加载x和y的值。然后,想象一下,我们有一个函数和,它和x和y的值相加,一旦它们都被加载了。

它可能是这样的(很难看,不是吗)

这里有一些非常重要的东西——在这个片段中,我们将x和y作为未来的值,并表示了一个操作和sum(…)(从外部看)不关心x和y,或者两者都是或不能马上得到。

当然,这种粗糙的基于调用的方法还有很多值得期待的。这只是迈向理解未来价值的好处的第一步,而不用担心时间的问题。

Promise 值

让我们简要地看看我们如何用承诺来表达x+y的例子:

在这段代码中有两层promise。

“fetchX()”和“fetchY()被直接调用,它们返回的值(承诺!)被传递给sum(...)。这些promise所代表的潜在值现在可能已经准备好了,但是无论如何,每个promise都将其行为规范化为相同的。我们以一种时间独立的方式来解释x和y的值。它们是未来的价值,周期。

第二层是promisesum(...) 创建(通过Promise.all([ ... ]))和返回,我们通过调用来 then(...)。当sum(...)操作完成,我们的和未来的价值已经准备好了,我们可以打印出来。我们隐藏了在sum(...) 中等待x和y未来值的逻辑。

Note:

sum(…)内部,Promise.all([ … ])调用创建了一个promise(等待promiseXpromiseY 来解决),链式调用.then(...)来创建另一个promise,返回“values[0]+ values[1]”line立即执行(加上添加的结果)。因此,then(...)的调用,我们链式结束sum(...)的调用,在代码段的最后,实际上是在第二个promise的返回,而不是第一个创建的Promise.all([ ... ]),还有,虽然我们还没有结束第二个then(...),它也创造了另一个promise,我们选择了观察/使用它。本章后面的内容将在后面详细解释。

Promises,then(...) 的调用实际上可以有两个方法,第一个是实现(如上所示),第二个是拒绝:

如果在得到“x”或“y”的时候出现了问题,或者在添加的过程中出现了一些失败,那么可以保证“sum(…)”返回将被拒绝,第二个回调错误处理程序传递给then(...),它将收到来自promise拒绝的值。

因为Promises 封装了依赖于时间的状态——等待内在值的实现或拒绝——从外部来看,Promises 本身是时间独立的,因此可以以可预测的方式组合(组合),而不考虑下面的时间和结果。

而且,一旦一个Promise得到解决,它就会永远保持这种状态——在那个时候它就变成了一个不可改变的值——然后就可以在必要的时候多次被使用。

实际上你可以链式Promise非常有用

调用delay(2000)创造一个在2000ms实现的承诺,然后我们从第一个“then(…)”返回“实现回调,导致第二个then(...)的promise在等待2000ms执行。

Note: 因为Promise 是外部不可改变的,一旦解决了,现在就可以安全地将这个值传递给任何一方,因为它知道它不能被意外或恶意地修改。对于遵守诺言的多方来说,这一点尤其正确。一个政党不可能影响另一个政党遵守承诺决议的能力。不变性可能听起来像是一个学术话题,但它实际上是Promise 设计最基本和最重要的方面之一,不应该被随意地忽略。

Promise 和非Promise?

关于Promises的一个重要细节是确定是否某些值是实际的Promise。换句话说,它的值会像一个Promise吗?

我们知道Promises是由new Promise(…)语法,你可能会认为 p instanceof Promise 是一个充分的检查。好吧,不完全是。

主要是因为你可以从另一个浏览器窗口(例如iframe)获得一个 Promised的值,它有自己的承诺,不同于当前窗口或框架中的一个,并且该检查将无法识别Promise实例。

而且,一个库或框架可以选择发布它自己的Promises,而不是使用本地ES6中的Promise实现来实现它。事实上,你很可能会在没有任何Promise的老式浏览器中使用 Promises

Swallowing例外

如果在创建Promise的任何时候,或者在对其解析的观察中,抛出了一个JavaScript异常错误,比如“TypeError”或“ReferenceError”,那么这个异常就会被捕获,它将迫使这个Promise被拒绝。

例如:

但是如果还履行Promise有一个JS异常错误观察期间(在 then(…) 注册回调)?即使它不会丢失,你可能会发现它们处理的方式有点令人惊讶。直到你深入挖掘:

它看起来像“foo . bar()”真的是被吞没了。不过,它不是。然而,有一些更深层次的问题出现了,我们没能监听到。“p.then(…)调用本身会返回另一个promise,而这个promise将会被“TypeError”异常所拒绝。

处理未捕获异常

还有其他的方法,很多人会说这些方法是更好的。 常见的建议是应该有一个 Promise应该增加一个done(…),这实际上标志着Promise链的“done”。done(…)不要创建并返回一个承诺,所以回调传递到done(…),很明显,他们并没有把问题报告给一个不存在的链式Promise。

在未捕获的错误条件下,你通常可以得到这样的处理:在done(..)中有任何异常(. .)拒绝处理程序将被抛出为一个全局未捕获的错误(通常在开发人员控制台):

在ES8里 Async/await会发生什么

JavaScript ES8引入async/await使工作的工作更容易。我们将简要介绍async/await提供的可能性,以及如何利用它们来编写异步代码。 因此,让我们看看async/await如何工作

使用“async”函数声明定义一个异步函数。这样的函数返回一个AsyncFunction对象,“AsyncFunction”对象表示执行代码的异步函数,该函数包含在该函数中。

当调用async函数时,它返回一个Promise 。当async函数返回一个值时,这不是一个PromisePromise 将会被自动创建,它将通过函数的返回值来解析。当async 函数抛出异常时,Promise 将被抛出的值拒绝。 一个async 函数可以包含一个“等待”表达式,该表达式暂停执行该函数并等待已传递的承诺的解析,然后恢复async函数的执行并返回解析值。 你可以把JavaScript的“承诺”看作是Java的“未来”或“c#”的任务。 async/await 的目的是简化使用承诺的行为。 让我们看一下下面的例子:

同样,抛出异常的函数等价于返回已被拒绝的承诺的函数:

await关键字只能在async 函数中使用,并允许您同步等待一个承诺。如果我们在async 函数之外使用承诺,我们仍然需要使用“then”回调:

你还可以使用“async函数表达式”来定义async函数。一个async函数表达式非常类似,它的语法和async函数语句差不多。异步函数表达式和异步函数语句之间的主要区别是函数名,它可以在异步函数表达式中省略,以创建匿名函数。一个async函数表达式可以作为一个IIFE(立即调用的函数表达式)来使用,当它被定义时就运行。 它看起来像这样:

It looks like this:

更重要的是,在所有主流浏览器中都支持async/await:

如果这个兼容性不是你想要的,那么也有几个JS的转换者像BabelTypeScript

在一天结束的时候,重要的是不要盲目地选择“最新”的方法来编写异步代码。理解异步JavaScript的内部原理是很重要的,了解它为什么如此重要,并深入理解您选择的方法的内部原理。每种方法都有利有弊。

编写高度可维护的、非脆弱的异步代码的5个技巧

  1. 干净代码: 使用async/await允许您编写更少的代码。每次使用async/await你跳过一些不必要的步骤:写。然后,创建一个匿名函数来处理响应,从该回调中命名响应。例如:

与:

2. 错误处理: Async/await 使得可以使用相同的代码结构来处理同步和异步错误—著名的try / catch语句。让我们看看它是如何做出Promises的:

与:

  1. 条件:async/await编写条件代码要简单得多

与:

4. 堆栈帧:async/await不同的是,从一个承诺链返回的错误堆栈没有给出错误发生的位置。看看下面的:

与:

5. Debugging: 如果你使用了promises,你知道调试它们是一场噩梦。例如,如果在a中设置断点。然后,阻塞并使用诸如 “stop-over”之类的调试快捷方式,调试器将不会移动到下面。然后,因为它仅通过同步代码 “steps”。 通过“async /等待”,您可以逐步等待调用,就像它们是正常的同步函数一样。 写 ** JavaScript代码很重要 不仅适用于应用程序本身,也适用于库的

例如, SessionStack库记录web应用程序/网站中的所有内容:所有DOM更改、用户交互、JavaScript异常、堆栈跟踪、失败的网络请求和调试消息。 这一切都必须发生在你的生产环境中,而不影响任何用户体验。我们需要对代码进行大量优化,并尽可能使其异步,这样我们就可以增加事件循环处理的事件的数量。 而不仅仅是库!当您在SessionStack中重播一个用户会话时,我们必须在问题发生时在您的用户的浏览器中呈现所有发生的事情,我们必须重构整个状态,允许您在会话时间轴上来回跳转。为了使这成为可能,我们大量使用了JavaScript提供的异步机会。

有一个免费的计划可以让你 开始免费.

资源: