ES6 中的 Generator * 最终如何处理异步操作/async
最基础的
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 }
- 定义
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
不是函数,不是函数,不是函数Hello()
不会立即出发执行,而是一上来就暂停- 每次
h.next()
都会打破暂停状态去执行,直到遇到下一个yield
或者return
- 遇到
yield
时,会执行yeild
后面的表达式,并返回执行之后的值,然后再次进入暂停状态,此时done: false
。- 遇到
return
时,会返回值,执行结束,即done: true
- 每次
h.next()
的返回值永远都是{value: ... , done: ...}
的形式
Generator
最终如何处理异步操作
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 个文件内容
})
Iterator 遍历器
ES6 中引入了很多此前没有但是却非常重要的概念,
Iterator
就是其中一个。Iterator
对象是一个指针对象,实现类似于单项链表的数据结构,通过next()
将指针指向下一个节点
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]
属性的数据类型
在 ES6 中,原生具有
[Symbol.iterator]
属性数据类型有:数组、某些类似数组的对象(如arguments
、NodeList
)、Set
和Map
。其中,Set
和Map
也是 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 完全不一样
生成Iterator
对象
定义一个数组,然后生成数组的
Iterator
对象
const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]() // 通过执行 [Symbol.iterator] 的属性值(函数)来返回一个 iterator 对象
现在生成了
iterator
,那么该如何使用它呢 ———— 有两种方式:next
和for...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)
}
Generator 的具体应用
介绍了
Generator
可以让执行处于暂停状态,并且知道了Generator
返回的是一个Iterator
对象
next
和yield
参数传递
我们之前已经知道,
yield
具有返回数据的功能,如下代码。yield
后面的数据被返回,存放到返回结果中的value
属性中。这算是一个方向的参数传递。
function* G() {
yield 100
}
const g = G()
console.log( g.next() ) // {value: 100, done: false}
还有另外一个方向的参数传递,就是
next
向yield
传递,如下代码
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...of
在Iterator
对象的操作之前已经介绍过了,不过这里用一个非常好的例子来展示一下。用简单几行代码实现斐波那契数列。通过之前学过的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) // 报错
Thunk 函数
要想让
Generator
和异步操作产生联系,就必须过thunk
函数这一关。这一关过了之后,立即就可以着手异步操作的事情,因此大家再坚持坚持。至于thunk
函数是什么,下文会详细演示。
一个普通的异步函数
就用 nodejs 中读取文件的函数为例
fs.readFile('data1.json', 'utf-8', (err, data) => {
// 获取文件内容
})
其实这个写法就是将三个参数都传递给
fs.readFile
这个方法,其中最后一个参数是一个callback
函数。这种函数叫做 多参数函数,我们接下来做一个改造
封装成一个thunk
函数
改造的代码如下所示。不过是不是感觉越改造越复杂了?不过请相信:你看到的复杂仅仅是表面的,这一点东西变的复杂,是为了让以后更加复杂的东西变得简单。对于个体而言,随性比较简单,遵守规则比较复杂;但是对于整体(包含很多个体)而言,大家都随性就不好控制了,而大家都遵守规则就很容易管理
const thunk = function (fileName, codeType) {
// 返回一个只接受 callback 参数的函数
return function (callback) {
fs.readFile(fileName, codeType, callback)
}
}
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
// 获取文件内容
})
- 执行
const readFileThunk = thunk('data1.json', 'utf-8')
返回的其实是一个函数readFileThunk
这个函数,只接受一个参数,而且这个参数是一个callback
函数
thunk
函数的特点
就上上面的代码,我们经过对传统的异步操作函数进行封装,得到一个只有一个参数的函数,而且这个参数是一个
callback
函数,那这就是一个thunk
函数。就像上面代码中readFileThunk
一样。
使用thunkify
库
上面代码的封装,是我们手动来做的,但是没遇到一个情况就需要手动做吗?在这个开源的时代当让不会这样,直接使用第三方的
thunkify
就好了。首先要安装
npm i thunkify --save
,然后在代码的最上方引用const thunkify = require('thunkify')
。最后,上面我们手动写的代码,完全可以简化成这几行,非常简单!
const thunk = thunkify(fs.readFile)
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
// 获取文件内容
})
Generator 与异步操作
在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)
})
})
也许上面的代码给你带来的感觉并不好,第一它逻辑复杂,第二它也不是那么易读、简洁呀,用
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
库
使用之前请安装
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)
koa 中使用 Generator
koa 是一个 nodejs 开发的 web 框架,所谓 web 框架就是处理 http 请求的。开源的 nodejs 开发的 web 框架最初是 express。
我们此前说过,既然是处理 http 请求,是一种网络操作,肯定就会用到异步操作。express 使用的异步操作是传统的
callbck
,而 koa 用的是我们刚刚讲的Generator
(koav1.x
用的是Generator
,已经被广泛使用,而 koav2.x
用到了 ES7 中的async-await
,不过因为 ES7 没有正式发布,所以 koav2.x
也没有正式发布,不过可以试用)koa 是由 express 的原班开发人员开发的,比 express 更加简洁易用,因此 koa 是目前最为推荐的 nodejs web 框架。阿里前不久就依赖于 koa 开发了自己的 nodejs web 框架 egg
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) // 12345
但是如果用 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 的这种应用机制是如何实现的
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); // 12345
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(next){
this.body += '3';
});
app.listen();
Generator 的本质是什么?是否取代了 callback
其实标题中的问题,是一个伪命题,因为
Generator
和callback
根本没有任何关系,只是我们通过一些方式(而且是很复杂的方式)强行将他俩产生了关系,才会有现在的Generator
处理异步。
Generator
的本质
暂停 这个词 —“暂停”才是
Generator
的本质 —只有Generator
能让一段程序执行到指定的位置先暂停,然后再启动,再暂停,再启动而这个 暂停 就很容易让它和异步操作产生联系,因为我们在处理异步操作时,即需要一种“开始读取文件,然后暂停一下,等着文件读取完了,再干嘛干嘛...”这样的需求。因此将
Generator
和异步操作联系在一起,并且产生一些比较简明的解决方案,这是顺其自然的事儿,大家要想明白这个道理。不过,JS 还是 JS,单线程还是单线程,异步还是异步,
callback
还是callback
。这一切都不会因为有一个Generator
而有任何变化。
和callback
的结合
之前在介绍
Promise
的最后,拿Promise
和callback
做过一些比较,最后发现Promise
其实是利用了callback
才能实现的。而这里,Generator
也必须利用callback
才能实现。拿介绍
co
时的代码举例(代码如下),如果yield
后面用的是thunk
函数,那么thunk
函数需要的就是一个callback
参数。如果yield
后面用的是Promise
对象,Promise
和callback
的联系之前已经介绍过了。
co(function* () {
const r1 = yield readFilePromise('some1.json')
console.log(r1) // 打印第 1 个文件内容
const r2 = yield readFileThunk('some2.json')
console.log(r2) // 打印第 2 个文件内容
})
因此,
Generator
离不开callback
,Promise
离不开callback
,异步也离不开callback
。
本文来自博客园,作者:JackieDYH,转载请注明原文链接:https://www.cnblogs.com/JackieDYH/p/17634534.html