概述
本文介绍一下async await以及co库的原理,算是一点心得,最近几个月搞了一些事,但是都没有写成博客,这段时间集中整理出来。
JavaScript中的async await本身也是一个语法糖,本身async await也是基于Promise实现的,使用其的最大好处是能让异步代码看上去像同步代码一样更容易理解。
async await的基本用法
回顾一下JavaScript中的异步代码怎么写,以jQuery的get方法为例。
1 | $.get(url, (data) => { |
好了,所有人都知道先输出'hello world 2'
再输出'hello world 1'
,但是回调函数嵌套太多时就会引起回调地狱,尤其是在NodeJS中。
后来发展的过程中出现了很多优秀的解决回调地狱问题的库,其中Promise脱颖而出,最终成为标准,现代浏览器以及NodeJS早就开始支持Promise了,它的用法如下。
1 | new Promise((resolve, reject) => { |
有关Promise的内容可以参考之前的文章,网上也有不少相关资料,它的出现可以说很方便地解决了回调嵌套过多的问题,但是这样的代码看上去还是令人不舒服(其实也还好),于是社区各路大神又开始献计献策,他们从别的语言中得到了灵感,于是async await被引入到了JavaScript中。
相对Promise而言,async await的用法看上去就舒服很多了。
1 | async function foo() { |
这里可以看到相比一般函数而言,这里多了async和await两个关键字进行修饰,其中async用于修饰function关键字,表示这是一个异步函数,而await呢,实际上后面跟的是一个Promise(前面说过async await是基于Promise实现的)。
具体是怎样的效果呢,f1
和f2
两个函数实际上返回的是一个Promise,假定f1
需要0.5s才能resolve,f2
需要0.7s才能resolve,则当foo
执行到f1
时,会等0.5s直到它resolve了值,0.5s之后输出结果,再执行下一句,f2
在0.7s之后resolve了一个值,输出之后,再将相加的结果返回,也就是是说最终foo
执行需要大约1.2s左右的时间。
具体如下例所示。
1 | function f1() { |
注意上面代码的最后一行,foo
执行后这里跟了一个then
方法,聪明的读者一定想到了,async函数返回的结果实际上也是一个Promise。
async await的基本用法就是这样,再来看一下foo
函数,里面的异步是不是更容易理解呢。
不过知其然还要知其所以然,我们一开始就说过,async await只是一个语法糖,只要使用generator和Promise的话,再加上一点点的黑魔法,我们能实现个八九不离十的效果出来。
什么,generator你也忘得差不多了?
生成器函数复习
generator之前也写过一篇文章,虽然很浅显,但是足够我们用了,在我们继续async await之前,先来复习一下它吧。
1 | function* bar() { |
以上就是一个生成器的基本用法,其中有三点需要注意的地方。
- 定义生成器函数的时候function关键字后面要加星号,即
function* foo
的形式,function *foo
的形式也可以,但是个人偏好第一种,因为这样直观地区别开了生成器函数和普通函数。 - 关键字yield是用于向外“产出”值的地方,生成器函数被调用后返回的实际上是一个可遍历的对象,通过该对象的next方法可以得到yield产出的值。这里我们第一次调用next,产出的值是3,也就是
yield 3
对应的{ value: 3, done: false}
,同理第二次调用产出的是4,最后一次呢?如果有return则是return的值,跟上例一样,如果没有呢?则为undefined,注意每次产出的结果是一个对象,该对象中有两个属性,分别是value和done,value的意思大家都知道了,done呢?其实就是表示当前生成器对象是否已经遍历完了,可以看到第三次调用时,done已经变成了true。 - 最后一点是yield表达式的值的问题,注意
yield 3
并不表示n1
等于3,yield表达式的值,是下一次next被调用时传入的值,所以可以看到第二次调用next时我们传入了5,所以n1
实际上等于5,同理n2
等于6,所以最终返回的结果是11,这点一定要注意。
除此之外generator还经常和for of一起用,关于generator的更多知识大家可以去查找相关资料,这里就不多做介绍了,接下来我们开始来学习一个重要的相关库,一个新的黑魔法的大门就要向我们敞开了。
co库及其源码浅析
co库是一个利用generator和Promise让异步代码更直观的辅助库,它的使用形式如下。
1 | import co from 'co'; |
这种形式的写法和上面使用async await的写法几乎是一样的,如果使用async await的话,代码如下。
1 | (async function () { |
co库虽然看上去很神奇,但是它的原理却并不复杂,它的工作机制主要是利用generator会转让控制权来实现的。
可以看到co库接收了一个generator函数,实际上co本身也只是一个高阶函数,传入其中的generator函数的执行会由这个高阶函数来控制。
它会一个阶段一个阶段地执行generator,每一个阶段执行完成之后,它又会通过Promise的then方法,将值传给generator中当前的yield表达式,这样控制权就又交回给了generator,如此往复执行,知道generator生成的值的done为true为止。
以下就是一个简单的co库的实现。
1 | const co = function (gen) { |
很明显,传进来的gen是一个generator函数,并且首先就被执行生成了一个generator对象,然后就是一个名为next
的IIFE,IIFE首先会执行生成器对象的next
方法,直到stage.done
为true
(生成器遍历结束)之前,如果他会去执行next
返回回来的Promise的then
方法,而传入then
方法中的参数就是名为next
的IIFE自身,这种巧妙的设计使得stage.value
(某个yield的出来的Promise)在resolve
之前,generator对象不会继续往后执行,直到stage.value.then(next)
被执行之后,再次执行g.next(val)
,这时才会将控制权交还给generator对象。
如果读者还是觉得上面的解释比较绕口,可以将上述代码复制粘贴到控制台,然后再传一个generator函数给co,在程序执行之初打上断点,通过一步一步调试的方法了解整个执行过程,多调试几遍,再结合上面的文字,相信很容易就能理解co库的原理。
await单独使用的情况
值得一提的是,async await并不是一直都是成双成对出现的,我们也可以只是用await来处理异步操作,比如如下代码。
1 | const result = await new Promise((resolve, reject) => { |
上述代码实际上会在1s之后输出'hello await'
,但熟悉JavaScript执行原理的同学们肯定会想,按理说result
应该是异步返回的,console.log(result)
不应该输出undefined
吗?
实际上await单独使用也是一个语法糖,上面的代码会被JavaScript引擎预处理为一个IIFE,最终执行的代码如下所示。
1 | (async () => {void (result = await new Promise((resolve, reject) => { |
可以看到我们单独使用await时,代码片段最终还是被包裹成了一个async await函数。
总结
async await关键字本质上和co的实现是类似的,它并没有引入更特殊的机制,它的出现只是为了让我们的异步代码看上去更易于理解,目前nodejs的web主流库之一koa2,就大量利用了async await的写法。
本文主要介绍了一下async await的使用和co库的实现原理,希望能对看到这篇文章的朋友,尤其是初学者有所启发,零外如有谬误,欢迎指正。