此篇是JavaScript的工作原理的第四篇,其它三篇可以看这里:

  • 【译】JavaScript的工作原理:引擎,运行时和调用堆栈的概述
  • 【译】JavaScript的工作原理:V8引擎内部+关于如何编写优化代码的5个技巧
  • 【译】JavaScript的工作原理:内存管理和4种常见的内存泄漏

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

为什么单线程有局限性?

在第一篇文章中,我们提到过一个问题:当调用栈中含有需要长时间运行的函数调用的时候会发生什么。 想象一下,例如,当浏览器中运行着一个复杂的图片转换算法。 在这个时候,堆栈中正好有函数在执行,浏览器此时不能做任何事情。此时,他被阻塞了。这意味着它不能渲染,不能运行其他代码,他被卡住了,没有任何响应。这就带来了一个问题,你的程序不再是高效的了。 你的程序没有相应了。 在某些情况下,这没有什么大不了的,但是这可能会造成更加严重的问题。一旦浏览器在调用栈中同时运行太多的任务的时候,浏览器会很长时间停止响应。在那个时候,大多数浏览器会抛出一个错误,询问是否终止网页。

这很丑陋且它完全摧毁了程序的用户体验。

Javascript程序的构建模块

你可能会在单一的 .js 文件中书写 JavaScript 程序,但是程序是由多个代码块组成的,当前,只有一个代码块在运行,其它代码块将在随后运行。最常见的块状单元是函数。 许多 JavaScript 新的开发者可能需要理解的问题是之后运行表示的是并不是必须立即在现在之后就执行。换句话说即,根据定义,现在不能够运行完毕的任务将会异步完成,这样你就不会不经意间遇到以上提及的 UI 阻塞。 看下面的代码:

// ajax 为一个库提供的任意 ajax 函数

可能你已经知道标准的 ajax 请求不会完全同步执行完毕,意即在代码运行阶段,ajax(..) 函数不会返回任何值给 response 变量

获得异步函数返回值的一个简单方法是使用回调函数。

ajax

只是要注意一点:即使可以也永远不要发起同步 ajax 请求。如果发起同步 ajax 请求,JavaScript 程序的 UI 将会被阻塞-用户不能够点击,输入数据,跳转或者滚动。任何用户交互都会被阻塞。这是非常糟糕。

以下示例代码,但请别这样做,这会毁掉网页:

// 假设你使用 jQuery

我们以 Ajax 请求为例。你可以异步执行任意代码。

你可以使用 setTimeout(callback, milliseconds) 函数来异步执行代码。setTimeout 函数会在之后的某个时刻触发事件(定时器)。如下代码:

function 

控制台输出如下:

first

剖析事件循环

我们这儿从一个奇怪的声明开始——尽管允许异步 JavaScript 代码(就像上例讨论的setTimeout),但在ES6之前,JavaScript本身实际上从来没有任何内置异步的概念,JavaScript引擎在任何给定时刻只执行一个块

对于更多的JavaScript引擎怎么工作的,可以看系列文章的第一篇

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

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

这意味着JS引擎只是任意JS代码的按需执行环境,是宿主环境处理事件运行及结果。

例如,当 JavaScript 程序发出 Ajax 请求从服务器获取一些数据时,在函数(“回调”)中设置“response”代码,JS引擎告诉宿主环境:"我现在要推迟执行,但当完成那个网络请求时,会返回一些数据,请回调这个函数并给数据传给它"。

然后浏览器将侦听来自网络的响应,当监听到网络请求返回内容时,浏览器通过将回调函数插入事件循环来调度要执行的回调函数。以下是示意图:

您可以在我们之前的文章中阅读有关内存堆和调用堆栈的更多信息。

这些Web api是什么?从本质上说,它们是无法访问的线程,只能调用它们。它们是浏览器的并发部分。如果你是一个Node.js开发者,这些就是 c++ 的 Api。

那么事件循环究竟是什么呢?

事件循环有一个简单的工作——监视调用堆栈和回调队列。如果调用堆栈为空,它将从队列中获取第一个事件,并将其推送到调用堆栈,这将有效地运行它。

这样的迭代在事件循环中称为 (tick标记),每个事件只是一个函数回调。

console

让我们执行这份代码看看发生了什么:

1.初始状态都为空,浏览器console面板为空,调用堆栈为空。

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

3.`console.log(Hi)`被执行。

4.`console.log('Hi')`从调用堆栈中移除。

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

6.`setTimeout(function cb1() { … })`被执行,浏览器通过它的Web APIS创建了一个计时器,为你的代码计时。

7.这个`setTimeout(function cb1() { … })`调用计时器它本身的函数是已经执行完成,从调用堆栈中移除。

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

9.`console.log('Bye')`被执行。

10.`console.log('Bye')`从调用堆栈中移除。

11.在至少5000ms后,定时器执行完成后,把`cb1`回调函数添加到回调队列里面。

12.事件循环把`cb1`从回调队列中取出,添加到调用堆栈中。

13.`cb1`被执行,把`console.log('cb1')`添加调用堆栈中。

14.`console.log('cb1')` 被执行。

15.`console.log('cb1')`从调用堆栈中移除。

16.`cb1`从调用堆栈中移除。

整体过程回顾:

比较值得注意的是,ES6指定了事件循环应该怎么运行。这意味着在技术范围内,他是属于JS引擎的职责范围内,不再仅仅扮演宿主环境的角色。这种变化的一个主要原因是ES6中引入了Promises,因为后者需要对事件循环队列上的调度操作更直接,控制更细粒度(稍后我们将更详细地讨论它们)

setTimeout(…)怎么工作的

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

setTimeout

这不是意味着myCallback将在1000ms后执行,而是在1000ms后myCallback将被添加到回调队列里面去,这个队列可能也有其他比较早被添加的事件正在等待,这个时候,你的回调就必须要等待。

有不少文章和教程说在JavaScript中开始使用异步编程的时候,都建议使用setTimeout(callback,0),那么现在你知道了事件循环的机制和setTimeout怎么运行的,调用setTimeout 0毫秒作为第二个参数只是推迟回调将它放到回调队列中,直到调用堆栈是空的。

看下下面的代码:

console

尽管等待时间设置成了0ms,这个浏览器打印的结果如下:

Hi
Bye
callback

ES6中的任务队列是什么?

在ES6的介绍中有一个新的叫做“任务队列”的概念,它是事件循环队列上面的一层,最常见的是在promise处理异步方式的时候。 现在只讨论这个概念,以便在讨论带有Promises的异步行为时,能够了解 Promises 是如何调度和处理。

想象一下:这个任务队列是附加到事件循环队列中每个标记(一次从回调队里里面取到数据后,放到调用堆栈执行的过程)末尾的队列,某些异步操作可能发生在事件循环的一个标记期间,不会导致一个全新的事件被添加到事件循环队列中,而是将一个项目(即任务)添加到当前标记的任务队列的末尾。

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

一个任务还可能创建更多任务添加到同一队列的末尾。理论上,任务“循环”(不断添加其他任务的任等等)可以无限运行,从而使程序无法获得转移到下一个事件循环标记的必要资源。从概念上讲,这类似于在代码中表示长时间运行或无限循环(如while (true) ..)。

任务有点像 setTimeout(callback, 0) “hack”,但其实现方式是引入一个定义更明确、更有保证的顺序:稍后执行,但越快越好。

回调

正如你已经知道的,回调是到目前为止JavaScript程序中表达和管理异步最常见的方法。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,除了一些基本都是在回调异步基础上编写的。 但是回调函数还是有一些缺点,开发者们试图探索更好的异步模式。但是,如果不了解底层的过程,就不可能有效地使用任何抽象出来的异步模式。 在下一章中,我们将深入探讨这些抽象,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是值得推荐的。

嵌套回调

看下下面的代码:

listen

我们组成了三个函数内嵌到一起的链式嵌套,每一个函数代表在异步系列里面的一步。 这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与嵌套/缩进几乎没有任何关系,这是一个更深层次的问题。 首先,我们等待“单击”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次重复所有操作。 乍一看,这段代码似乎可以将其异步过程对应到以下多个函数顺序执行的步骤:

listen

然后:

setTimeout

再然后:

ajax

最后:

if 

所以这种同步的方式去表达你的异步嵌套代码,是不是更自然一些?一定有这样的方法,对吧?

Promise

看下下面的代码:

var 

非常的直观,这个x和y相加之和通过console.log打印出来。如果,x和y的值还没有赋上,仍然需要求值,怎么办? 例如,需要从服务器取回x和y的值,然后才能在表达式中使用它们。假设我们有一个函数loadX和loadY,它们分别从服务器加载x和yy的值。然后,一旦x和y都被加载,我们有一个函数sum,它对x和y的值进行求和。 它可能是这样的:

function 

这段代码中有一些非常重要的东西,我们将x和y作为异步获取的值,并且执行了一个函数sum(…)(从外部),它不关心x或y,也不关心它们是否立即可用。

当然,这种基于回调的粗略方法还有很多不足之处。 这只是一个我们不必判断对于异步请求的值的处理方式一个小步骤而已。

Promise Value

简单的看一下,我们怎么用promise表达x+y:

function 

在这个代码中有两层promise。 fetchX() 和 fetchY() 直接被调用,他们返回的值(promise)传入到了sum(...)。这个promise所代表的基础值无论是现在或者将来都可以准备就绪。但每个promise都会将其行为规范化,我们以与时间无关的方式推理x和y的值。某一段时间内,他们是一个将来的值。

这第二层promise是sum(...)创造的(通过 Promise.all([ ... ])),然后返回promise。通过调用then(…)来等待。当 sum(…) 操作完成时,sum 传入的两个 Promise 都执行完后,可以打印出来了。这里隐藏了在sum(…)中等待x和y未来值的逻辑。

注意: 在这个sum(...)里面,这个Promise.all([...])调用创建一个 promise(等待 promiseX 和 promiseY 它们resolve)。然后链式调用 .then(...)方法里再的创建了另一个 Promise,然后把(values[0] + values[1]) 进行求和并返回。

因此,我们在sum(...)末尾调用then(...)方法——实际上是在返回的第二个 Promise 上的运行,而不是由Promise.all([ ... ])创建的Promise。此外,虽然没有在第二个 Promise 结束时再调用 then方法 ,其时这里也创建一个 Promise。

Promise.then(…) 实际上可以使用两个函数,第一个函数用于执行成功的操作,第二个函数用于处理失败的操作: 如果在获取x或y时出现错误,或者在添加过程中出现某种失败,sum(…) 返回的 Promise将被拒绝,传递给then(…)的第二个回调错误处理程序将从 Promise 接收失败的信息。

从外部看,由于 Promise 封装了依赖于时间的状态(等待底层值的完成或拒绝,Promise 本身是与时间无关的),它可以按照可预测的方式组成,不需要开发者关心时序或底层的结果。 Promise一旦resolve,此刻在外部他就成了不可变的值——然后就可以根据需求多次观察。

链式调用对于你来说是真的有用:

function 

调用delay(2000)创建一个2000ms后将被实现(fulfill)的promise,然后通过第一个then(...)来接收回调信号,在这里面也返回一个promise,通过第二个then(...)的promise来等待2000ms的promise。

注意: 因为一个Promise一旦被resolved,在外面看来就成了不可变了,所以现在可以把它安全的传递到程序的任何地方。因为它不能被意外地或恶意地修改,这一点在多个地方观察一个promise时尤其正确。一方不可能影响另一方观察promise结果的能力,不变性听起来像是一个学术话题,但它实际上是promise设计最基本和最重要的方面之一,不应该被随意忽略。

用不用Promise

关于 Promise 的一个重要细节是要确定某个值是否是一个实际的Promise。换句话说,它是否具有像Promise一样行为?

我们知道 Promise 是由new Promise(…)语法构造的,你可能认为p instanceof Promise是一个足够可以判断的类型,嗯,不完全是!

这主要是因为可以从另一个浏览器窗口(例如iframe)接收Promise值,而该窗口或框架具有自己的Promise值,与当前窗口或框架中的Promise 值不同,所以该检查将无法识别 Promise 实例。

此外,库或框架可以选择性的封装自己的Promise,而不使用原生 ES6 的Promise 来实现。事实上,很可能在老浏览器的库中没有 Promise。

捕获错误和异常

如果在 Promise 创建中,出现了一个javascript异常错误(TypeError或者ReferenceError),这个异常会被捕捉,并且使这个 promise 被拒绝。 比如:

var 

但是,如果在调用 then(…)方法中出现了JS异常错误,那么会发生什么情况呢?即使它不会丢失,你可能会发现它们的处理方式有点令人吃惊,直到你挖得更深一点:

var 

看起来foo.bar()中的异常确实被吞噬了,不过,它不是。然而,还有一些更深层次的问题,我们没有注意到。 p.then(…) 调用本身返回另一个 Promise,该 Promise 将被 TypeError 异常拒绝。

处理未捕获异常

许多人会说,还有其他更好的方法。

一个常见的建议是,Promise 应该添加一个 done(…),这实际上是将 Promise 链标记为 “done”。done(…)不会创建并返回 Promise ,因此传递给 done(..) 的回调显然不会将问题报告给不存在的链接 Promise 。

Promise 对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个 done 方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

var 

ES8中有什么变化 ?Async/await (异步/等待)

JavaScript ES8引入了async/await,这使得使用Promise的工作更容易。这里将简要介绍async/await 提供的可能性以及如何利用它们编写异步代码。

使用 async 声明异步函数。这个函数返回一个AsyncFunction 对象。AsyncFunction 对象表示该函数中包含的代码是异步函数。

调用使用 async 声明函数时,它返回一个Promise。当这个函数返回一个值时,这个值只是一个普通值而已,这个函数内部将自动创建一个promise,并使用函数返回的值进行解析。当这个函数抛出异常时,Promise 将被抛出的值拒绝。

使用 async 声明函数时可以包含一个await符号,await暂停这个函数的执行并等待传递的 Promise 的解析完成,然后恢复这个函数的执行并返回解析后的值。

async/wait 的目的是简化使用promise的行为

看下下面的列子:

// Just a standard JavaScript function

类似地,函数抛出异常相当于函数返回的promise被reject了:

//这两个函数一样

await关键词只能使用在async函数中,允许去同步等待一个promise执行。如果在async外面使用promise,仍然需要使用then回调。

async 

还可以使用“异步函数表达式”定义异步函数。异步函数表达式与异步函数语句非常相似,语法也几乎相同。异步函数表达式和异步函数语句之间的主要区别是函数名,可以在异步函数表达式中省略函数名来创建匿名函数。异步函数表达式可以用作声明(立即调用的函数表达式),一旦定义它就会运行。

就像这样:

var 

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

最后,重要的是不要盲目选择编写异步代码的“最新”方法。理解异步 JavaScript 的内部结构非常重要,了解为什么异步JavaScript如此关键,并深入理解所选择的方法的内部结构。与编程中的其他方法一样,每种方法都有优点和缺点。

编写高度可维护、稳定的异步代码

1.简化代码

使用 async/await 可以编写更少的代码。每次使用async/await时,都会跳过一些不必·要的步骤:使用.then,创建一个匿名函数来处理响应:

// `rp` is a request-promise function.

与:

// `rp` is a request-promise function.

2.错误处理

Async/wait 可以使用相同的代码结构(众所周知的try/catch语句)处理同步和异步错误。看看它是如何与 Promise 结合的:

function 

与:

async 

3.条件处理

用async/ wait编写条件代码要简单得多:

function 

与:

async 

4.错误堆栈

与 async/await不同,从 Promise 链返回的错误堆栈不提供错误发生在哪里。看看下面这些:

function 

与:

async 

5.调试

如果你使用过 Promise,那么你知道调试它们是一场噩梦。例如,如果在一个程序中设置了一个断点,然后阻塞并使用调试快捷方式(如“停止”),调试器将不会移动到下面,因为它只“逐步”执行同步代码。使用async/wait,您可以逐步完成wait调用,就像它们是正常的同步函数一样。

循环中需要调用异步怎么确保执行完再执行其他的_JavaScript的工作原理:事件循环及异步编程...相关推荐

  1. APScheduler如何设置任务不并发(即第一个任务执行完再执行下一个)?

    APScheduler如何设置任务不并发(即第一个任务执行完再执行下一个)? 1.软件环境 2.问题描述 3.解决方法 4.结果预览 FAQ 1.`APScheduler`如果某个任务挂掉了,整个定时 ...

  2. js 循环 等待异步执行完再执行_JS异步执行机制——事件循环(Event Loop)

    JS异步执行机制--事件循环(Event Loop) 本文首发地址: 前端基础 | JS异步执行机制--事件循环(Event Loop)​www.brandhuang.com 先祭出一段代码,你清楚它 ...

  3. js等待异步执行完再执行,js如何让代码同步执行

    JS方法怎么同步执行 . 方法1内部用了var表示内部变量执行完一次后会自动释放:方法2内的变量cc不能与方法1重名:示例代码如下测试可以varstr='';functioncfun(obj){str ...

  4. vue中函数执行完再执行另一个函数

    转载自:https://blog.csdn.net/MonsteriU/article/details/103782752 promise实现 function1(){// 你的逻辑代码 return ...

  5. js等待异步执行完再执行_Spring Boot 之异步执行方法

    前言: 最近的时候遇到一个需求,就是当服务器接到请求并不需要任务执行完成才返回结果,可以立即返回结果,让任务异步的去执行.开始考虑是直接启一个新的线程去执行任务或者把任务提交到一个线程池去执行,这两种 ...

  6. js等待ajax执行完,js等待方法执行完再执行

    您好 哈哈,我刚解决了一个这样的问题如果直接for然后ajax,肯定是不行的因为ajax是异步调用,for循环都循环完了,ajax说不定才执行了第一个调用当然用同步调用的方式可以解决,但同步会阻塞浏览 ...

  7. 主线程等待子线程执行完再执行,几种实现方式性能分析比较

    直接上图: 综上图分析,故可得出初步结论,在线程数量较少的情况下,CyclicBarrier略占优,当线程数量大于10000的时候,join的方式反而比较节省耗时.

  8. python等待执行完毕 再继续执行_如何等待一个函数内部异步执行完毕再执行另外一个函数?...

    问题描述 有两个函数f1和f2,其中f1中有异步操作,执行完f1接着执行f2,因为f1内部有异步操作,所以会先执行完f2,再执行f1里的异步,如何等待f1异步执行完再执行f2. 备注:执行方式为f1( ...

  9. JavaScript 如何工作的: 事件循环和异步编程的崛起 + 5 个关于如何使用 async/await 编写更好的技巧...

    原文地址:How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding wi ...

  10. [译] JavaScript 如何工作的: 事件循环和异步编程的崛起 + 5 个关于如何使用 async/await 编写更好的技巧...

    原文地址:How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding wi ...

最新文章

  1. GATB的使用小例子test.cpp
  2. 如何使用知识图谱增强信息检索模型?
  3. boost::core模块实现分配构造
  4. bbb sdk6编译流程
  5. InnoDB架构,一幅图秒懂!
  6. Win32 控件篇(6)
  7. vueCli3中使用代理,点击页面的刷新按钮时报错
  8. opencv中的安全指针和指针对齐
  9. ubuntu18 安装 mysql5.7
  10. 错误解决:src/cpp/cuda.hpp:  fatal error: cuda.h
  11. android下最强的3款pdf阅读器测评
  12. 网络性能监测-系统监视器
  13. golang实现的文件服务器
  14. ps钢笔工具的详细讲解
  15. 【WIFI专题】Wifi 2.4G及5G频段各信道的中心频率及主要国家的分布情况
  16. 想要玩转数字影音?可以找Adobe帮忙呀
  17. 【信号处理】因果系统的理解
  18. 生活充满欺骗,不由你不信
  19. 【路漫漫其修远兮,吾将上下而求索】
  20. Python+selenium 实现自动投稿、自动发布哔哩哔哩B站短视频实例演示

热门文章

  1. 【身份证识别】基于matlab BP神经网络身份证号码识别【含Matlab源码 1344期】
  2. 【PID优化】基于matlab遗传算法PID控制器优化设计【含Matlab源码 1144期】
  3. 【多目标优化求解】基于matlab粒子群算法求解智能微电网多目标优化问题【含Matlab源码 383期】
  4. ai智能和大数据测试_测试版可帮助您根据自己的条件创建数据和AI平台
  5. 人工智能+社交 csdn_关于AI和社交媒体虚假信息,我们需要尽快进行三大讨论
  6. 10g添加用户 oracle_oracle10g下新建/删除用户
  7. linux查看创建目录命令,Linux菜鸟——常见命令一 查看及创建目录文件等命令
  8. springboot内置浏览器_springboot-为内置tomcat设置虚拟目录
  9. Twow ndows,笔者教您syswow64 【设置步骤】 的详细方法_
  10. php 多个箭头,php连续的两个箭头-〉是什么意思??