长文,要有耐心

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

当调用堆栈中有函数调用需要花费大量时间来处理时会发生什么?

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

当调用堆栈有函数要执行时,浏览器不能做任何其他事情——它被阻塞了。这意味着浏览器不能渲染,不能运行任何其他代码,只是卡住了。那么你的应用 UI 界面就卡住了,用户体验也就不那么好了。

JavaScript程序的构建块

你可能在单个.js文件中编写 JavaScript 应用程序,但可以肯定的是,你的程序由几个块组成,其中只有一个正在执行,其余的将在稍后执行。最常见的块单元是函数。

大多数刚接触JavaScript的开发人员似乎都有这样的问题,就是认为所有函数都是同步完成,没有考虑的异步的情况。如下例子:

你可能知道标准 Ajax 请求不是同步完成的,这说明在代码执行时 Ajax(..) 函数还没有返回任何值来分配给变量 response。

一种等待异步函数返回的结果简单的方式就是 回调函数:

注意:实际上可以设置同步Ajax请求,但永远不要那样做。如果设置同步Ajax请求,应用程序的界面将被阻塞——用户将无法单击、输入数据、导航或滚动。这将阻止任何用户交互,这是一种可怕的做法。

以下是同步 Ajax 地,但是请千万不要这样做:

这里使用Ajax请求作为示例,你可以让任何代码块异步执行。

这可以通过 setTimeout(callback,milliseconds) 函数来完成。setTimeout 函数的作用是设置一个回调函数milliseconds后执行,如下:

function first() { console.log('first');}function second() { console.log('second');}function third() { console.log('third');}first();setTimeout(second, 1000); // Invoke `second` after 1000msthird();

输出:

firstthirdsecond

解析事件循环

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

那么,是谁告诉JS引擎执行程序的代码块呢?实际上,JS引擎并不是单独运行的——它是在一个宿主环境中运行的,对于大多数开发人员来说,宿主环境就是典型的web浏览器或Node.js。实际上,现在JavaScript被嵌入到各种各样的设备中,从机器人到灯泡,每个设备代表 JS 引擎的不同类型的托管环境。所有环境中的共同点是一个称为事件循环的内置机制,它处理程序的多个块在一段时间内通过调用JS引擎的执行。

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

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

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

这些Web api是什么?从本质上说,它们是无法访问的线程,只能调用它们。它们是浏览器的并发部分。

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

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

1.初始化状态都为空,浏览器控制台是空的的,调用堆栈也是空的

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

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

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

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

6. setTimeout(function cb1() { ... }) 执行,浏览器创建一个计时器计时,这个作为Web api的一部分。

7. setTimeout(function cb1() { ... })本身执行完成,并从调用堆栈中删除。

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

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

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

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

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

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

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

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

16. cb1 从调用堆栈中移除

快速回顾:

值得注意的是,ES6指定了事件循环应该如何工作,这意味着在技术上它属于JS引擎的职责范围,不再仅仅扮演宿主环境的角色。这种变化的一个主要原因是ES6中引入了 Promises,因为ES6需要对事件循环队列上的调度操作进行直接、细度的控制。

setTimeout(…) 是怎么工作的

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

setTimeout(myCallback, 1000);

这并不意味着myCallback将在1000毫秒后就立马执行,而是在1000毫秒后,myCallback被添加到队列中。但是,如果队列有其他事件在前面添加回调刚必须等待前后的执行完后在执行myCallback。

有不少的文章和教程上开始使用异步JavaScript代码,建议用setTimeout(回调,0),现在你知道事件循环和setTimeout是如何工作的:调用setTimeout 0毫秒作为第二个参数只是推迟回调将它放到回调队列中,直到调用堆栈是空的。

请看下面的代码:

console.log('Hi');setTimeout(function() { console.log('callback');}, 0);console.log('Bye');

虽然等待时间被设置为0 ms,但在浏览器控制台的结果如下:

HiByecallback

ES6的任务队列是什么?

ES6中引入了一个名为“任务队列”的概念。它是事件循环队列上的一个层。最为常见在Promises 处理的异步方式。

想像一下:任务队列是一个附加到事件循环队列中每个标记末尾的队列。某些异步操作可能发生在事件循环的一个标记期间,不会导致一个全新的事件被添加到事件循环队列中,而是将一个项目(即任务)添加到当前标记的任务队列的末尾。

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

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

回调

正如你已经知道的,回调是到目前为止JavaScript程序中表达和管理异步最常见的方法。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,除了一些基本都是在回调异步基础上编写的。

然而回调方式还是有一些缺点,许多开发人员都在试图找到更好的异步模式。但是,如果不了解底层的内容,就不可能有效地使用任何抽象出来的异步模式。

嵌套回调

请看以下代码:

有一个由三个函数组成的链嵌套在一起,每个函数表示异步系列中的一个步骤。

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

首先,我们等待“单击”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次重复所有操作。

乍一看,这段代码似乎可以将其异步性自然地对应到以下顺序步骤:

listen('click', function (e) { // ..});

然后:

setTimeout(function(){ // ..}, 500);

接着:

ajax('https://api.example.com/endpoint', function (text){ // ..});

最后:

if (text == "hello") { doSomething();}else if (text == "world") { doSomethingElse();}

因此,这种连续的方式来表示异步代码似乎更自然.

Promises

请看下面的代码:

var x = 1;var y = 2;console.log(x + y);

这非常简单:它对x和y的值进行求和,并将其打印到控制台。但是,如果x或y的值丢失了,仍然需要求值,要怎么办?

例如,需要从服务器取回x和y的值,然后才能在表达式中使用它们。假设我们有一个函数loadX和loadY,它们分别从服务器加载x和y的值。然后,一旦x和y都被加载,假设我们有一个函数sum,它对x和y的值进行求和。

它可能看起来像这样:

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

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

Promise Value

用Promise来重写上例:

在这个代码片段中有两层Promise。

fetchX 和 fetchY 先直接调用,返回一个promise,传给 sum。 sum 创建并返回一个Promise,通过调用 then 等待 Promise,完成后,sum 已经准备好了(resolve),将会打印出来。

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

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

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

Promise.then(…) 实际上可以使用两个函数,第一个函数用于执行成功的操作,第二个函数用于处理失败的操作:

如果在获取x或y时出现错误,或者在添加过程中出现某种失败,sum(…) 返回的 Promise将被拒绝,传递给 then(…) 的第二个回调错误处理程序将从 Promise 接收失败的信息。

从外部看,由于 Promise 封装了依赖于时间的状态(等待底层值的完成或拒绝,Promise 本身是与时间无关的),它可以按照可预测的方式组成,不需要开发者关心时序或底层的结果。一旦 Promise 决议,此刻它就成为了外部不可变的值。

可链接调用 Promise 真的很有用:

创建一个延迟2000ms内完成的 Promise ,然后我们从第一个then(...)回调中返回,这会导致第二个then(...)等待 2000ms。

注意:因为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 被拒绝。

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

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

处理未捕获异常

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

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

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

ES8中改进了什么 ?Async/await (异步/等待)

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

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

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

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

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

让看看下面的例子:

function getNumber1() { return Promise.resolve('374');}// 这个函数与getNumber1相同async function getNumber2() { return 374;}

类似地,抛出异常的函数等价于返回被拒绝的 Promise 的函数:

 function f1() { return Promise.reject('Some error');}async function f2() { throw 'Some error';}

await 关键字只能在异步函数中使用,并允许同步等待 Promise。如果在 async 函数之外使用 Promise,仍然需要使用 then 回调:

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

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

编写高度可维护性、非易碎异步代码的5个技巧

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

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

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

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

与:

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


欢迎关注

循环中需要调用异步怎么确保执行完再执行其他的_什么是事件循环和异步编程?5种使用async/await更好地编码方式!...相关推荐

  1. JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!...

    此篇是 JavaScript是如何工作的第四篇,其它三篇可以看这里: JavaScript是如何工作的:引擎,运行时和调用堆栈的概述! JavaScript是如何工作的:深入V8引擎&编写优化 ...

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  10. [前端记录] --- vue axios 等调用完再执行后面的语句

    vue 整合echarts的时候,是先axios 调用后台接口,获取数据,再将数据渲染到页面. 一开始的问题是:第一次请求完,页面没数据,再请求一次才出现数据 分析到最后,发下是由于axios是异步请 ...

最新文章

  1. javascript——Array类型
  2. Qt Style Sheets(qt样式表)
  3. 添加halcon图像显示控件_初级应用实战来咯!C#联合Halcon读取图像,带讲解!!...
  4. FOR ALL ENTRIES IN内表排序、排重对性能的影响
  5. 企业家Scott Gerber:小公司应用开发的十条建议
  6. 网站站长综合seo在线查询工具源码
  7. ElasticSearch核心基础之索引管理
  8. 黑客游戏未发先被黑:游戏开发商Ubisoft 和 Crytek遭勒索攻击
  9. 8.4 bert的压缩讲解 意境级
  10. Google Colab使用笔记
  11. 【Axure】Axure RP 9 下载、短期试用破解安装和汉化步骤 —— 可供安装参考,短期试用,目前授权码已逐渐失效
  12. 一个检测PC机软硬件系统信息的工具——EVEREST
  13. cad插入块_CAD施工制图常见问答(一)
  14. USACO 2019 February Contest Platinum T3: Mowing Mischief
  15. 笔记本怎么自己装系统?u盘装系统windows7教程图解
  16. 2021年初级会计职称《初级会计实务》考试真题
  17. win7注册表常用设置
  18. 150行Python代码模拟太阳系行星运转
  19. 在Word中隐藏文字
  20. 二进制基带信号的时域特性

热门文章

  1. 尽量用iterator代替const_iterator
  2. VMware:虚拟化技术为运营商消除隐患
  3. 加载本地文件为String类型
  4. Airflow 重跑dag中部分失败的任务
  5. BZOJ1014 [JSOI2008]火星人
  6. canvas绘制经典星空连线效果
  7. 【网络】为什么我执行了发布操作,但是线上的资源并没有更新?
  8. Jenkins实现生产环境部署文件的回滚操作(Windows)
  9. 全面剖析Linux库文件路径的添加
  10. 北大oj-1423- Big Number