循环中需要调用异步怎么确保执行完再执行其他的_从底层看前端(五)—— JavaScript代码执行(1)…

  • Post author:
  • Post category:java


1ebe28b218034420d8acf4c547ae4aee.png

今天我们来讲一讲JavaScript的执行。

首先我们考虑一下,如果我们是浏览器或者node的开发者,我们该如何使用JavaScript引擎呢?

当拿到一段JavaScript代码时,浏览器或者node环境首先要做的是:传递给JavaScript引擎,并且要求它去执行。

然而,执行JavaScript并非一锤子买卖,宿主环境当遇到一些事件时,会继续把一段代码传递给JavaScript引擎去执行。此外,我们可能还会提供API给JavaScript引擎,比如setTimeout这样的API,它会允许JavaScript在特定的时机去执行代码。

所以,我们首先应该形成一个感性的认识:一个JavaScript引擎会常驻于内存之中,它等待着我们(宿主)把JavaScript代码或者函数传递给它执行。

在ES3或者更早的版本中,JavaScript本身还没有异步执行代码的能力,这就意味着,宿主环境传递给JavaScript引擎一段代码,引擎就把代码直接按顺序就执行了,这个任务也就是宿主发起的任务。

但是,在ES5之后,JavaScript引入了Promise。这样,不需要浏览器安排,JavaScript引擎本身也可以发起任务了。

由于我们这里主要讲JavaScript语言,那么也就采纳JSC引擎的术语,我们把宿主发起的任务成为

宏观任务

。把JavaScript引擎发起的任务称为

微观任务


宏任务和微任务

JavaScript引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以在node术语中,也会把这个部分称为

事件循环

不过,术语本身并非我们讨论的重点内容,我们在这里把重点放在事件循环的原理上。在底层的C/C++代码中,这个事件循环是一个跑在独立线程中的循环,我们用伪代码来表示,大概是这样的:

while 

我们可以看到,整个循环做的事情基本上就是反复’等待-执行’。当然,实际代码中并没有这么简单,还要判断循环是否结束,宏观任务队列等逻辑。

这里每次执行的过程,其实都是一个宏观任务,我们可以大概理解:宏观任务队列就相当于事件循环。

在宏观任务中,JavaScript的Promise还会产生异步代码,JavaScript必须保证这些异步代码在一个宏任务中完成,因此,每个宏任务中又包含了一个微观任务队列。

dd5899f10402dc47a2cae4499f3d238f.png

有了宏观任务和微观任务机制,我们就可以实现JavaScript引擎级和宿主级的任务了,例如:Promise永远在队列尾部添加微观任务。setTimeout等宿主API,则会添加在宏观任务。

接下来,我们来详细介绍下Promise。


Promise

Promise是JavaScript语言提供的一种标准化的异步管理方式,他的总体思想是,需要进行io,等待或者其它异步操作的函数,不返回真实的结果,而返回一个承诺,函数的调用方可以在合适的时机,选择等待这个成若兑现(通过Promis的then方法的回调)。

Promise的基本用法示例如下:

function 

这段代码定义了一个sleep,它的作用是等待传入参数指定的时长。

Promise的then回调是一个异步执行过程,下面我们就来研究一下Promise函数中的执行顺序,我们来看一段代码示例:

var 

我们执行这段代码后,注意输出的顺序是a,b,c,在进入console.info(‘b’)之前,毫无疑问已经得到了resolve,但是Promise的resolve始终是异步操作,所以c无法出现在b之前。

接下来我们试试跟setTimeout混用的Promise。

在这段代码中,我设置了两段不互相干的异步操作:通过setTimeout执行console.log(‘d’),通过Promise执行console.log(‘c’)。

var 

我们发现,不论代码顺序如何,d必定发生在c之后,因为Promise产生的是JavaScript引擎内部的微任务,而setTimeout是浏览器API,它产生宏任务。

为了理解微任务始终先于宏任务,我们在这里设计一个实验:执行一个耗时一秒的Promise。

setTimeout

这里我们强制了1秒的执行耗时,这样,我们可以确保任务c2是在d之后被添加到任务队列。

我们可以看到,即使耗时一秒的c1执行完毕,再enque的c2,仍然先于d执行了,这很好地解释了微任务优先的原理。

通过一系列实验,我们可以总结一下如何分析异步执行的顺序:

首先我们分析有多少个宏任务;

在每个宏任务中,分析有多少个微任务;

根据调用次数,确定宏任务中的微观任务执行次序;

根据宏任务的出发规则和调用次序,确定宏任务的执行次序;

确定整个顺序;

我们再来看一个稍微复杂的例子:

function 

这是一段非常常用的封装方法,利用Promise把setTimeout封装成可以用于异步的函数。

我们首先来看,setTimeout把整个代码分割成了两个宏观任务,这里不管是5秒还是0秒都是一样的。

第一个宏任务中,包含了先后同步执行的console.log(‘a’)和console.log(‘b’)

setTimeout后,第二个宏任务执行调用了resolve,然后then中的代码异步得到执行,所以调用了console.log(‘c’),最终输出的顺序才是:abc。

Promise是JavaScript中的一个定义,但是实际编写代码时,我们可以发现,它似乎并不比回调的书写方式更加简单,但是从ES6开始,我们有了async和await,这个语法改进跟Promise配合,能够有效地改善代码结构。


新特性:async/await

async/await是ES2016新加入的特性,它提供了for,if等代码结构来编写异步的方式。它的运行时基础是Promise,面对这种比较新的特性,我们先来看一下基本用法。

async函数必定返回Promise,我们把所有返回Promise的函数都可以认为是异步函数。

async函数是一种特殊的语法,特征是在function关键字前加上async关键字,这样就定义了一个async函数,我们可以再其中使用await来等待一个Promise。

function 

这段代码利用了我们之前定义的sleep函数。在异步函数foo中,我们调用sleep。

async函数强大之处在于,它可以是嵌套的。我们在定义了一批原子操作的情况下,可以利用async函数组合出新的async函数。

function 

这里foo2用await调用了两次异步函数foo,可以看到,如果我们把sleep这样的异步操作放入某一个框架或者库中,使用者几乎不需要理解Promise的概念就可以进行异步编程了。

此外,gennerator/iterator也常常被跟异步一起来讲,我们必须说明的是,gennerator/iterator并非异步代码,只是在缺少async/await的时候,一些框架(比如co)使用这样的特性来模拟async/await。

gennerator并非被设计成实现异步,所以有了async/await之后,gennerator/iterator模拟异步的方法应该被废弃。