JavaScript 异步编程的前世今生(下)

时间:2022-10-18 13:33:56

ES6 中的 Generator

在 ES6 出现之前,基本都是各式各样类似Promise的解决方案来处理异步操作的代码逻辑,但是 ES6 的Generator却给异步操作又提供了新的思路,马上就有人给出了如何用Generator来更加优雅的处理异步操作。

本节内容概述

  • Generator简介
  • Generator最终如何处理异步操作
  • 接下来...

Generator简介

先来一段最基础的Generator代码

function* Hello() {
yield 100
yield (function () {return 200})()
return 300
} var h = Hello()
console.log(typeof h) // object console.log(h.next()) // { value: 100, done: false }
console.log(h.next()) // { value: 200, done: false }
console.log(h.next()) // { value: 300, done: true }
console.log(h.next()) // { value: undefined, done: true }

在 nodejs 环境执行这段代码,打印出来的数据都在代码注释中了,也可以自己去试试。将这段代码简单分析一下吧

  • 定义Generator时,需要使用function*,其他的和定义函数一样。内部使用yield,至于yield的用处以后再说
  • 执行var h = Hello()生成一个Generator对象,经验验证typeof h发现不是普通的函数
  • 执行Hello()之后,Hello内部的代码不会立即执行,而是出于一个暂停状态
  • 执行第一个h.next()时,会激活刚才的暂停状态,开始执行Hello内部的语句,但是,直到遇到yield语句。一旦遇到yield语句时,它就会将yield后面的表达式执行,并返回执行的结果,然后又立即进入暂停状态。
  • 因此第一个console.log(h.next())打印出来的是{ value: 100, done: false }value是第一个yield返回的值,done: false表示目前处于暂停状态,尚未执行结束,还可以再继续往下执行。
  • 执行第二个h.next()和第一个一样,不在赘述。此时会执行完第二个yield后面的表达式并返回结果,然后再次进入暂停状态
  • 执行第三个h.next()时,程序会打破暂停状态,继续往下执行,但是遇到的不是yield而是return。这就预示着,即将执行结束了。因此最后返回的是{ value: 300, done: true }done: true表示执行结束,无法再继续往下执行了。
  • 再去执行第四次h.next()时,就只能得到{ value: undefined, done: true },因为已经结束,没有返回值了。

一口气分析下来,发现并不是那么简单,虽然这只是一个最最简单的Generator入门代码 ———— 可见Generator的学习成本多高 ———— 但是一旦学会,那将受用无穷!别着急,跟着我的节奏慢慢来,一行一行代码看,你会很快深入了解Genarator

但是,你要详细看一下上面的所有步骤,争取把我写的每一步都搞明白。如果搞不明白细节,至少要明白以下几个要点:

  • Generator不是函数,不是函数,不是函数
  • Hello()不会立即出发执行,而是一上来就暂停
  • 每次h.next()都会打破暂停状态去执行,直到遇到下一个yield或者return
  • 遇到yield时,会执行yeild后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时done: false
  • 遇到return时,会返回值,执行结束,即done: true
  • 每次h.next()的返回值永远都是{value: ... , done: ...}的形式

Generator最终如何处理异步操作

上面只是一个最基本最简单的介绍,但是我们看不到任何与异步操作相关的事情,那我们接下来就先展示一下最终我们将使用Generator如何做异步操作。

之前讲解Promise时候,依次读取多个文件,我们是这么操作的(看不明白的需要回炉重造哈),主要是使用then做链式操作。

readFilePromise('some1.json').then(data => {
console.log(data) // 打印第 1 个文件内容
return readFilePromise('some2.json')
}).then(data => {
console.log(data) // 打印第 2 个文件内容
return readFilePromise('some3.json')
}).then(data => {
console.log(data) // 打印第 3 个文件内容
return readFilePromise('some4.json')
}).then(data=> {
console.log(data) // 打印第 4 个文件内容
})

而如果学会Generator那么读取多个文件就是如下这样写。先不要管如何实现的,光看一看代码,你就能比较出哪个更加简洁、更加易读、更加所谓的优雅!

co(function* () {
const r1 = yield readFilePromise('some1.json')
console.log(r1) // 打印第 1 个文件内容
const r2 = yield readFilePromise('some2.json')
console.log(r2) // 打印第 2 个文件内容
const r3 = yield readFilePromise('some3.json')
console.log(r3) // 打印第 3 个文件内容
const r4 = yield readFilePromise('some4.json')
console.log(r4) // 打印第 4 个文件内容
})

不过,要学到这一步,还需要很长的路要走。不过不要惊慌,也不要请如来佛祖,跟着我的节奏来,认真看,一天包教包会是没问题的!

接下来...

接下来我们不会立刻讲解如何使用Generator做异步操作,而是看一看Generator是一个什么东西!说来话长,这要从 ES6 的另一个概念Iterator说起。

Iterator 遍历器

ES6 中引入了很多此前没有但是却非常重要的概念,Iterator就是其中一个。Iterator对象是一个指针对象,实现类似于单项链表的数据结构,通过next()将指针指向下一个节点 ———— 这里也就是先简单做一个概念性的介绍,后面将通过实例为大家演示。

本节演示的代码可参考这里

本节内容概述

  • 简介Symbol数据类型
  • 具有[Symbol.iterator]属性的数据类型
  • 生成Iterator对象
  • Generator返回的也是Iterator对象
  • 接下来...

简介Symbol数据类型

Symbol是一个特殊的数据类型,和number string等并列,详细的教程可参考阮一峰老师 ES6 入门的 Symbol 篇。先看两句程序

console.log(Array.prototype.slice)  // [Function: slice]
console.log(Array.prototype[Symbol.iterator]) // [Function: values]

数组的slice属性大家都比较熟悉了,就是一个函数,可以通过Array.prototype.slice得到。这里的slice是一个字符串,但是我们获取Array.prototype[Symbol.iterator]可以得到一个函数,只不过这里的[Symbol.iterator]Symbol数据类型,不是字符串。但是没关系,Symbol数据类型也可以作为对象属性的key。如下:

var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}

在此小节中,你只需要知道[Symbol.iterator]是一个特殊的数据类型Symbol类型,但是也可以像number string类型一样,作为对象的属性key来使用

原生具有[Symbol.iterator]属性的数据类型

在 ES6 中,原生具有[Symbol.iterator]属性数据类型有:数组、某些类似数组的对象(如argumentsNodeList)、SetMap。其中,SetMap也是 ES6 中新增的数据类型。

// 数组
console.log([1, 2, 3][Symbol.iterator]) // function values() { [native code] }
// 某些类似数组的对象,NoeList
console.log(document.getElementsByTagName('div')[Symbol.iterator]) // function values() { [native code] }

原生具有[Symbol.iterator]属性数据类型有一个特点,就是可以使用for...of来取值,例如

var item
for (item of [100, 200, 300]) {
console.log(item)
}
// 打印出:100 200 300
// 注意,这里每次获取的 item 是数组的 value,而不是 index ,这一点和 传统 for 循环以及 for...in 完全不一样

而具有[Symbol.iterator]属性的对象,都可以一键生成一个Iterator对象。如何生成以及生成之后什么样子,还有生成之后的作用,下文分解。

不要着急,也不要跳过本文的任何步骤,一步一步跟着我的节奏来看。

生成Iterator对象

定义一个数组,然后生成数组的Iterator对象

const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]() // 通过执行 [Symbol.iterator] 的属性值(函数)来返回一个 iterator 对象

好,现在生成了iterator,那么该如何使用它呢 ———— 有两种方式:nextfor...of

先说第一种,next

console.log(iterator.next())  // { value: 100, done: false }
console.log(iterator.next()) // { value: 200, done: false }
console.log(iterator.next()) // { value: 300, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

看到这里,再结合上一节内容,是不是似曾相识的感觉?(额,没有的话,那你就回去重新看上一节的内容吧) iterator对象可以通过next()方法逐步获取每个元素的值,以{ value: ..., done: ... }形式返回,value就是值,done表示是否到已经获取完成。

再说第二种,for...of

let i
for (i of iterator) {
console.log(i)
}
// 打印:100 200 300

上面使用for...of遍历iterator对象,可以直接将其值获取出来。这里的“值”就对应着上面next()返回的结果的value属性

Generator返回的也是Iterator对象

看到这里,你大体也应该明白了,上一节演示的Generator,就是生成一个Iterator对象。因此才会有next(),也可以通过for...of来遍历。拿出上一节的例子再做一次演示:

function* Hello() {
yield 100
yield (function () {return 200})()
return 300
}
const h = Hello()
console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]]

执行const h = Hello()得到的就是一个iterator对象,因为h[Symbol.iterator]是有值的。既然是iterator对象,那么就可以使用next()for...of进行操作

console.log(h.next())  // { value: 100, done: false }
console.log(h.next()) // { value: 200, done: false }
console.log(h.next()) // { value: 300, done: false }
console.log(h.next()) // { value: undefined, done: true } let i
for (i of h) {
console.log(i)
}

接下来...

这一节我们花费很大力气,从Iterator又回归到了Generator,目的就是为了看看Generator到底是一个什么东西。了解其本质,才能更好的使用它,否则总有一种抓瞎的感觉。

接下来我们就Generator具体有哪些使用场景。

Generator 的具体应用

前面用两节的内容介绍了Generator可以让执行处于暂停状态,并且知道了Generator返回的是一个Iterator对象,这一节就详细介绍一下Generator的一些基本用法。

本节演示的代码可参考这里

本节内容概述

  • nextyield参数传递
  • for...of的应用示例
  • yield*语句
  • Generator中的this
  • 接下来...

nextyield参数传递

我们之前已经知道,yield具有返回数据的功能,如下代码。yield后面的数据被返回,存放到返回结果中的value属性中。这算是一个方向的参数传递。

function* G() {
yield 100
}
const g = G()
console.log( g.next() ) // {value: 100, done: false}

还有另外一个方向的参数传递,就是nextyield传递,如下代码。

function* G() {
const a = yield 100
console.log('a', a) // a aaa
const b = yield 200
console.log('b', b) // b bbb
const c = yield 300
console.log('c', c) // c ccc
}
const g = G()
g.next() // value: 100, done: false
g.next('aaa') // value: 200, done: false
g.next('bbb') // value: 300, done: false
g.next('ccc') // value: undefined, done: true

捋一捋上面代码的执行过程:

  • 执行第一个g.next()时,为传递任何参数,返回的{value: 100, done: false},这个应该没有疑问
  • 执行第二个g.next('aaa')时,传递的参数是'aaa',这个'aaa'就会被赋值到G内部的a标量中,然后执行console.log('a', a)打印出来,最后返回{value: 200, done: false}
  • 执行第三个、第四个时,道理都是完全一样的,大家自己捋一捋。

有一个要点需要注意,就g.next('aaa')是将'aaa'传递给上一个已经执行完了的yield语句前面的变量,而不是即将执行的yield前面的变量。这句话要能看明白,看不明白就说明刚才的代码你还没看懂,继续看。

for...of的应用示例

针对for...ofIterator对象的操作之前已经介绍过了,不过这里用一个非常好的例子来展示一下。用简单几行代码实现斐波那契数列。通过之前学过的Generator知识,应该不难解读这份代码。

function* fibonacci() {
let [prev, curr] = [0, 1]
for (;;) {
[prev, curr] = [curr, prev + curr]
// 将中间值通过 yield 返回,并且保留函数执行的状态,因此可以非常简单的实现 fibonacci
yield curr
}
}
for (let n of fibonacci()) {
if (n > 1000) {
break
}
console.log(n)
}

yield*语句

如果有两个Generator,想要在第一个中包含第二个,如下需求:

function* G1() {
yield 'a'
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}

针对以上两个Generator,我的需求是:一次输出a x y b,该如何做?有同学看到这里想起了刚刚学到的for..of可以实现————不错,确实可以实现(大家也可以想想到底该如何实现)

但是,这要演示一个更加简洁的方式yield*表达式

function* G1() {
yield 'a'
yield* G2() // 使用 yield* 执行 G2()
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
for (let item of G1()) {
console.log(item)
}

之前学过的yield后面会接一个普通的 JS 对象,而yield*后面会接一个Generator,而且会把它其中的yield按照规则来一步一步执行。如果有多个Generator串联使用的话(例如Koa源码中),用yield*来操作非常方便。

Generator中的this

对于以下这种写法,大家可能会和构造函数创建对象的写法产生混淆,这里一定要注意 —— Generator 不是函数,更不是构造函数

function* G() {}
const g = G()

而以下这种写法,更加不会成功。只有构造函数才会这么用,构造函数返回的是this,而Generator返回的是一个Iterator对象。完全是两码事,千万不要搞混了。

function* G() {
this.a = 10
}
const g = G()
console.log(g.a) // 报错

接下来...

本节基本介绍了Generator的最常见的用法,但是还是没有和咱们的最终目的————异步操作————沾上关系,而且现在看来有点八竿子打不着的关系。但是话说回来,这几节内容,你也学到了不少知识啊。

别急哈,即便是下一节,它们还不会有联系,再下一节就真相大白了。下一节我们又给出一个新概念————Thunk函数

Generator 与异步操作

这一节正式开始讲解Generator如何进行异步操作,以前我们花了好几节的时间各种打基础,现在估计大家也都等急了,好戏马上开始!

本节演示的代码可参考这里

本节内容概述

  • Genertor中使用thunk函数
  • 挨个读取两个文件的内容
  • 自驱动流程
  • 使用co
  • co库和Promise
  • 接下来...

Genertor中使用thunk函数

这个比较简单了,之前都讲过的,直接看代码即可。代码中表达的意思,是要依次读取两个文件的内容

const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1)
const r2 = yield readFileThunk('data2.json')
console.log(r2)
}

挨个读取两个文件的内容

接着以上的代码继续写,注释写的非常详细,大家自己去看,看完自己写代码亲身体验。

const g = gen()

// 试着打印 g.next() 这里一定要明白 value 是一个 thunk函数 ,否则下面的代码你都看不懂
// console.log( g.next() ) // g.next() 返回 {{ value: thunk函数, done: false }} // 下一行中,g.next().value 是一个 thunk 函数,它需要一个 callback 函数作为参数传递进去
g.next().value((err, data1) => {
// 这里的 data1 获取的就是第一个文件的内容。下一行中,g.next(data1) 可以将数据传递给上面的 r1 变量,此前已经讲过这种参数传递的形式
// 下一行中,g.next(data1).value 又是一个 thunk 函数,它又需要一个 callback 函数作为参数传递进去
g.next(data1).value((err, data2) => {
// 这里的 data2 获取的是第二个文件的内容,通过 g.next(data2) 将数据传递个上面的 r2 变量
g.next(data2)
})
})

上面 6 行左右的代码,却用了 6 行左右的注释来解释,可见代码的逻辑并不简单,不过你还是要去尽力理解,否则接下来的内容无法继续。再说,我已经写的那么详细了,你只要照着仔细看肯定能看明白的。

也许上面的代码给你带来的感觉并不好,第一它逻辑复杂,第二它也不是那么易读、简洁呀,用Generator实现异步操作就是这个样子的?———— 当然不是,继续往下看。

自驱动流程

以上代码中,读取两个文件的内容都是手动一行一行写的,而我们接下来要做一个自驱动的流程,定义好Generator的代码之后,就让它自动执行。完整的代码如下所示:

// 自动流程管理的函数
function run(generator) {
const g = generator()
function next(err, data) {
const result = g.next(data) // 返回 { value: thunk函数, done: ... }
if (result.done) {
// result.done 表示是否结束,如果结束了那就 return 作罢
return
}
result.value(next) // result.value 是一个 thunk 函数,需要一个 callback 函数作为参数,而 next 就是一个 callback 形式的函数
}
next() // 手动执行以启动第一次 next
} // 定义 Generator
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1.toString())
const r2 = yield readFileThunk('data2.json')
console.log(r2.toString())
} // 启动执行
run(gen)

其实这段代码和上面的手动编写读取两个文件内容的代码,原理上是一模一样的,只不过这里把流程驱动给封装起来了。我们简单分析一下这段代码

  • 最后一行run(gen)之后,进入run函数内部执行
  • const g = generator()创建Generator实例,然后定义一个next方法,并且立即执行next()
  • 注意这个next函数的参数是err, data两个,和我们fs.readFile用到的callback函数形式完全一样
  • 第一次执行next时,会执行const result = g.next(data),而g.next(data)返回的是{ value: thunk函数, done: ... }value是一个thunk函数,done表示是否结束
  • 如果done: true,那就直接return了,否则继续进行
  • result.value是一个thunk函数,需要接受一个callback函数作为参数传递进去,因此正好把next给传递进去,让next一直被执行下去

大家照着这个过程来捋一捋,不是特别麻烦,然后自己试着写完运行一下,基本就能了解了。

使用co

刚才我们定义了一个run还是来做自助流程管理,是不是每次使用都得写一遍run函数呢?———— 肯定不是的,直接用大名鼎鼎的co就好了。用Generator的工程师,肯定需要用到co,两者天生一对,难舍难分。

使用之前请安装npm i co --save,然后在文件开头引用const co = require('co')co到底有多好用,我们将刚才的代码用co重写,就变成了如下代码。非常简洁

// 定义 Generator
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1.toString())
const r2 = yield readFileThunk('data2.json')
console.log(r2.toString())
}
const c = co(gen)

而且const c = co(gen)返回的是一个Promise对象,可以接着这么写

c.then(data => {
console.log('结束')
})

co库和Promise

刚才提到co()最终返回的是Promise对象,后知后觉,我们已经忘记Promise好久了,现在要重新把它拾起来。如果使用co来处理Generator的话,其实yield后面可以跟thunk函数,也可以跟Promise对象。

thunk函数上文一直在演示,下面演示一下Promise对象的,也权当再回顾一下久别的Promise。其实从形式上和结果上,都跟thunk函数一样。

const readFilePromise = Q.denodeify(fs.readFile)

const gen = function* () {
const r1 = yield readFilePromise('data1.json')
console.log(r1.toString())
const r2 = yield readFilePromise('data2.json')
console.log(r2.toString())
} co(gen)

接下来...

经过了前几节的技术积累,我们用一节的时间就讲述了Generator如何进行异步操作。接下来要介绍一个开源社区中比较典型的使用Generator的框架 ———— Koa

koa 中使用 Generator

koa 是一个 nodejs 开发的 web 框架,所谓 web 框架就是处理 http 请求的。开源的 nodejs 开发的 web 框架最初是 express

我们此前说过,既然是处理 http 请求,是一种网络操作,肯定就会用到异步操作。express 使用的异步操作是传统的callbck,而 koa 用的是我们刚刚讲的Generator(koa v1.x用的是Generator,已经被广泛使用,而 koa v2.x用到了 ES7 中的async-await,不过因为 ES7 没有正式发布,所以 koa v2.x也没有正式发布,不过可以试用)

koa 是由 express 的原班开发人员开发的,比 express 更加简洁易用,因此 koa 是目前最为推荐的 nodejs web 框架。阿里前不久就依赖于 koa 开发了自己的 nodejs web 框架 egg

国内可以通过koa.bootcss.com查阅文档,不过这网站依赖了 Google 的服务,因此如果不*,估计会访问会很慢

提醒:如果你是初学Generator而且从来没有用过 koa ,那么这一节你如果看不懂,没有问题。看不懂就不要强求,可以忽略,继续往下看!

本节演示的代码可参考这里

本节内容概述

  • koa 中如何应用Generator
  • koa 的这种应用机制是如何实现的
  • 接下来...

koa 中如何应用Generator

koa 是一个 web 框架,处理 http 请求,但是这里我们不去管它如何处理 http 请求,而是直接关注它使用Genertor的部分————中间件。

例如,我们现在要用 3 个Generator输出12345,我们如下代码这么写。应该能看明白吧?看不明白回炉重造!

let info = ''
function* g1() {
info += '1' // 拼接 1
yield* g2() // 拼接 234
info += '5' // 拼接 5
}
function* g2() {
info += '2' // 拼接 2
yield* g3() // 拼接 3
info += '4' // 拼接 4
}
function* g3() {
info += '3' // 拼接 3
} var g = g1()
g.next()
console.log(info) //

但是如果用 koa 的 中间件 的思路来做,就需要如下这么写。

app.use(function *(next){
this.body = '1';
yield next;
this.body += '5';
console.log(this.body);
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(next){
this.body += '3';
});

解释几个关键点

  • app.use()中传入的每一个Generator就是一个 中间件,中间件按照传入的顺序排列,顺序不能乱
  • 每个中间件内部,next表示下一个中间件。yield next就是先将程序暂停,先去执行下一个中间件,等next被执行完之后,再回过头来执行当前代码的下一行。因此,koa 的中间件执行顺序是一种洋葱圈模型,不过这里看不懂也没问题。
  • 每个中间件内部,this可以共享变量。即第一个中间件改变了this的属性,在第二个中间件中可以看到效果。

koa 的这种应用机制是如何实现的

前方高能————上面介绍 koa 的中间价估计有些新人就开始蒙圈了,不过接下来还有更加有挑战难度的,就是以上这种方式是如何实现的。你就尽量去看,看懂了更好,看不懂也没关系————当然,你完全可以选择跳过本教程直接去看下一篇,这都 OK

加入我们自己实现一个简单的 koa ———— MyKoa ,那么仅需要几十行代码就可以搞定上面的问题。直接写代码,注意看重点部分的注释

class MyKoa extends Object {
constructor(props) {
super(props); // 存储所有的中间件
this.middlewares = []
} // 注入中间件
use (generator) {
this.middlewares.push(generator)
} // 执行中间件
listen () {
this._run()
} _run () {
const ctx = this
const middlewares = ctx.middlewares
co(function* () {
let prev = null
let i = middlewares.length
//从最后一个中间件到第一个中间件的顺序开始遍历
while (i--) {
// ctx 作为函数执行时的 this 才能保证多个中间件中数据的共享
//prev 将前面一个中间件传递给当前中间件,才使得中间件里面的 next 指向下一个中间件
prev = middlewares[i].call(ctx, prev);
}
//执行第一个中间件
yield prev;
})
}
}

最后我们执行代码实验一下效果

var app = new MyKoa();
app.use(function *(next){
this.body = '1';
yield next;
this.body += '5';
console.log(this.body); //
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(next){
this.body += '3';
});
app.listen();

接下来...

Generator的应用基本讲完,从一开始的基础到后面应用到异步操作,再到本节的高级应用 koa ,算是比较全面了。接下来,我们要再回到最初的起点,探讨Generator的本质,以及它和callback的关系。

还是那句话,搞明白原理,才能用的更加出色!

Generator 的本质是什么?是否取代了 callback

其实标题中的问题,是一个伪命题,因为Generatorcallback根本没有任何关系,只是我们通过一些方式(而且是很复杂的方式)强行将他俩产生了关系,才会有现在的Generator处理异步。

本节内容概述

  • Generator的本质
  • callback的结合

Generator的本质

介绍Generator第一节中,多次提到 暂停 这个词 ———— “暂停”才是Generator的本质 ———— 只有Generator能让一段程序执行到指定的位置先暂停,然后再启动,再暂停,再启动。

而这个 暂停 就很容易让它和异步操作产生联系,因为我们在处理异步操作时,即需要一种“开始读取文件,然后暂停一下,等着文件读取完了,再干嘛干嘛...”这样的需求。因此将Generator和异步操作联系在一起,并且产生一些比较简明的解决方案,这是顺其自然的事儿,大家要想明白这个道理。

不过,JS 还是 JS,单线程还是单线程,异步还是异步,callback还是callback。这一切都不会因为有一个Generator而有任何变化。

callback的结合

之前在介绍Promise的最后,拿Promisecallback做过一些比较,最后发现Promise其实是利用了callback才能实现的。而这里,Generator也必须利用callback才能实现。

拿介绍co时的代码举例(代码如下),如果yield后面用的是thunk函数,那么thunk函数需要的就是一个callback参数。如果yield后面用的是Promise对象,Promisecallback的联系之前已经介绍过了。

co(function* () {
const r1 = yield readFilePromise('some1.json')
console.log(r1) // 打印第 1 个文件内容
const r2 = yield readFileThunk('some2.json')
console.log(r2) // 打印第 2 个文件内容
})

因此,Generator离不开callbackPromise离不开callback,异步也离不开callback

ES7 中引入 async-await

前面介绍完了Generator的异步处理,可以说是跌跌撞撞,经过各种基础介绍和封装,好容易出了一个比较简洁的异步处理方案,学习成本非常高————这显然不是我们想要的!

因此,还未发布的 ES7 就干脆自己参照Generator封装了一套异步处理方案————async-await。说是参照,其实可以理解为是Generator的语法糖!

本节示例代码参照这里

本节内容概述

  • Generatorasync-await的对比
  • 使用async-await的不同和好处
  • 接下来...

Generatorasync-await的对比

先来一段Generator处理异步的代码,前面已经介绍过了,看不明白的再获取接着看。

co(function* () {
const r1 = yield readFilePromise('some1.json')
console.log(r1) // 打印第 1 个文件内容
const r2 = yield readFilePromise('some2.json')
console.log(r2) // 打印第 2 个文件内容
})

再来一段async-await的执行代码如下,两者做一个比较。

const readFilePromise = Q.denodeify(fs.readFile)

// 定义 async 函数
const readFileAsync = async function () {
const f1 = await readFilePromise('data1.json')
const f2 = await readFilePromise('data2.json')
console.log('data1.json', f1.toString())
console.log('data2.json', f2.toString()) return 'done' // 先忽略,后面会讲到
}
// 执行
const result = readFileAsync()

从上面两端代码比较看来,async function代替了function*await代替了yield,其他的再没有什么区别了。哦,还有,使用async-await时候不用再引用co这种第三方库了,直接执行即可。

使用async-await的不同和好处

第一,await后面不能再跟thunk函数,而必须跟一个Promise对象(因此,Promise才是异步的终极解决方案和未来)。跟其他类型的数据也OK,但是会直接同步执行,而不是异步。

第二,执行const result = readFileAsync()返回的是个Promise对象,而且上面代码中的return 'done'会直接被下面的then函数接收到

result.then(data => {
console.log(data) // done
})

第三,从代码的易读性来将,async-await更加易读简介,也更加符合代码的语意。而且还不用引用第三方库,也无需学习Generator那一堆东西,使用成本非常低。

因此,如果 ES7 正式发布了之后,强烈推荐使用async-await。但是现在尚未正式发布,从稳定性考虑,还是Generator更好一些。

接下来...

node v7 版本已经开始原生支持async-await了,不过 node 的目前稳定版本还是v6,尚不支持,怎么办?———— 当然是万能的babel!下一节就介绍。

如何在 nodejs v6.x版本中使用 async-await

本节介绍一下如何使用babel来让 node v6 版本也能运行async-await

本节内容概述

  • 安装必要的插件
  • 创建入口文件并执行

安装必要的插件

运行npm i babel-core babel-plugin-transform-runtime babel-preset-es2015 babel-preset-stage-3 babel-runtime --save安装一堆需要的插件。

然后在项目根目录创建.babelrc文件,文件内容编写为

{
"presets": ["stage-3", "es2015"],
"plugins": ["transform-runtime"]
}

创建入口文件并执行

加入你编写async-await的代码文件是test.js,那么你需要创建另一个文件,例如test-entry.js作为入口文件。入口文件内容编写为

require("babel-core/register");
require("./test.js");

然后直接运行node test-entry.js就可以了

总结

一周左右的业余时间总结完,写完,也是累得我够呛。不算什么体力活,但是天天的坐在书桌旁写这些东西也是很考验一个人的定力,没点耐性是肯定不行的 ———— 这算是获奖感言吗