66

这就是所谓的JavaScript异步!

 5 years ago
source link: http://developer.51cto.com/art/201811/586483.htm?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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 完全使用该语法。

什么是异步

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

QrYNvuz.jpg!web

异步

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

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

qiINriv.jpg!web

同步

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

回调函数callback

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

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

RNzYNf2.jpg!web

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

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

promise对象

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

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

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

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

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

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

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

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

FBbiuqv.jpg!web

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

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

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

协程

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

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

第一步,协程A开始执行。

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

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

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

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

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

function asnycJob() { 
 // ...其他代码 
 var f = yield readFile(fileA); 
 // ...其他代码 
} 

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

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

Generator 函数

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

function* gen(x){ 
 var y = yield x + 2; 
 return y; 
} 

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

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

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

上面代码中,调用 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); 
} 

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

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

var g = gen(); 
var result = g.next(); 
result.value.then( function(data){ 
 return data.json(); 
}).then( function(data){ 
 g.next(data); 
}); 

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

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

async-await

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

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

2eaY7nA.jpg!web

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

AjiU7bQ.jpg!web

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

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

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

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

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

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

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

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

zuERVj2.jpg!web

【责任编辑:庞桂玉 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK