深圳幻海软件技术有限公司 欢迎您!

这就是所谓的JavaScript异步!

2023-02-26

ECMAScript6(简称ES6)将JavaScript异步编程带入了一个全新的阶段。这篇文章的主题,就是介绍更强大、更完善的ES6异步编程方法。首先我们回顾一下javascript异步的发展历程。ES6以前:回调函数(callback):nodejsexpress中常用,ajax中常用。ES6:

ECMAScript 6(简称ES6)将 JavaScript 异步编程带入了一个全新的阶段。这篇文章的主题,就是介绍更强大、更完善的 ES6 异步编程方法。

首先我们回顾一下javascript异步的发展历程。

ES6 以前:

回调函数(callback):nodejs express 中常用,ajax中常用。

ES6:

promise对象:nodejs最早有bluebird promise的雏形,axios中常用。

generator函数:nodejs koa框架使用率很高。

ES7:

async/await语法:当前最常用的异步语法,nodejs koa2 完全使用该语法。

什么是异步

所谓"异步",简单说就是一个任务分成两段,先执行***段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。比如,有一个任务是读取文件进行处理,异步的执行过程就是下面这样。

 

 

异步

上图中,任务的***段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。

这种不连续的执行,就叫做异步。相应地,连续的执行,就叫做同步。

 

 

同步

上图就是同步的执行方式。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

回调函数callback

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字 callback,直译过来就是"重新调用"。

回调字面也好理解,就是先处理本体函数,再处理回调的函数,举个例子,方便大家理解。

 

 

上面的例子很好理解,首先执行主体函数A,打印结果:我是主题函数;

然后执行回调函数callback 也就是B,打印结果:我是回调函数。

promise对象

promise 对象用于一个异步操作的最终完成(或最终失败)及其结果的表示。

简单地说就是处理一个异步请求。我们经常会做些断言,如果我赢了你就嫁给我,如果输了我就嫁给你之类的断言。

这就是promise的中文含义:断言,一个成功,一个失败。

举个例子,方便大家理解:

promise构造函数的参数是一个函数,我们把它称为处理器函数。

处理器函数接收两个函数reslove和reject作为其参数,当异步操作顺利执行则执行reslove函数, 当异步操作中发生异常时,则执行reject函数。

通过resolve传入得的值,可以在then方法中获取到,通过reject传入的值可以在chatch方法中获取到。

因为then和catch都返回一个相同的promise对象,所以可以进行链式调用。

 

 

Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。

Promise 的***问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。

那么,有没有更好的写法呢?

协程

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

***步,协程A开始执行。

第二步,协程A执行到一半,进入暂停,执行权转移到协程B。

第三步,(一段时间后)协程B交还执行权。

第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下。

 

function asnycJob() { 
 // ...其他代码 
 var f = yield readFile(fileA); 
 // ...其他代码 
} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的***优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

Generator 函数

Generator 函数是协程在 ES6 的实现,***特点就是可以交出函数的执行权(即暂停执行)。

 

function* gen(x){ 
 var y = yield x + 2; 
 return y; 
} 
  • 1.
  • 2.
  • 3.
  • 4.

上面代码就是一个 Generator 函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号,以示区别。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下。

 

var g = gen(1); 
g.next() // { value: 3, done: false } 
g.next() // { value: undefined, done: true } 
  • 1.
  • 2.
  • 3.

上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器 )g 。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的***段),指向***个遇到的 yield 语句,上例是执行到 x + 2 为止。

换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

Generator 函数的用法

下面看看如何使用 Generator 函数,执行一个真实的异步任务。

 

var fetch = require('node-fetch'); 
function* gen(){ 
 var url = 'https://api.github.com/users/github'; 
 var result = yield fetch(url); 
 console.log(result.bio); 
} 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了 yield 命令。

执行这段代码的方法如下。

 

var g = gen(); 
var result = g.next(); 
result.value.then(function(data){ 
 return data.json(); 
}).then(function(data){ 
 g.next(data); 
}); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用 next 方法(第二行),执行异步任务的***阶段。由于 Fetch 模块返回的是一个 Promise 对象,因此要用 then 方法调用下一个next 方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行***阶段、何时执行第二阶段)。

async-await

async函数返回一个promise对象,如果在async函数中返回一个直接量,async会通过Promise.resolve封装成Promise对象。

我们可以通过调用promise对象的then方法,获取这个直接量。

 

 

那如过async函数不返回值,又会是怎么样呢?

 

 

await会暂停当前async的执行,await会阻塞代码的执行,直到await后的表达式处理完成,代码才能继续往下执行。

await后的表达式既可以是一个Promise对象,也可以是任何要等待的值。

如果await等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

上边你看到阻塞一词,不要惊慌,async/await只是一种语法糖,代码执行与多个callback嵌套调用没有区别。

本质并不是同步代码,它只是让你思考代码逻辑的时候能够以同步的思维去思考,避开回调地狱。

简而言之-async/await是以同步的思维去写异步的代码,所以async/await并不会影响node的并发数,大家可以大胆的应用到项目中去!

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

举个例子,方便大家理解: