ES6-Generator

概念

基本概念

Generator函数是ES6的异步编程解决方案.Generator函数可以理解成一个状态机,封装了多个内部状态.Generator函数有两个特征:一是function与函数名之间有一个星号;二是函数体内部使用yield语句定义不同的内部状态.

1
2
3
4
5
function* helloWorldGenerator(){
yield 'hello'
yield 'world'
return 'end'
}

上面的代码定义了一个Generator函数,它有三个状态:hello,world和return语句.

Generator函数的调用方法和普通函数一样,也是在函数名后加上一对括号,但是调用后,函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是Iterator.

调用Iterator对象的next方法,使得指针移向下一个状态.Generator函数是分段执行的,yield语句是函数暂停执行的标记,next方法可以恢复运行.

1
2
3
var hw = helloWorldGenerator()
console.log(hw.next(), hw.next(), hw.next())
//{value: 'hello', done: false} {value: 'world', done: false} {value: 'end', done: true}

yield表达式

yield语句是暂停标志,遇到yield语句就暂停后面的操作,并将紧跟在yield后的表达式的值作为返回的对象的value属性值.

如果没有yield语句,就一直运行到函数结束,直到return语句为止.

yield语句提供了手动的Lazy Evaluation语法功能.

Generator函数可以不用yield语句,就变成一个单纯暂缓执行函数,只有在调用next方法时才会执行.

1
2
3
4
5
function* f(){
console.log("执行了!")
}
var generator=f()
setTimeout(()=>{generator.next()},2000)//只有在调用next方法时才会执行

yield表达式只能用在Generator函数里,用在其他地方会报错.

与Iterator接口的关系

1
2
3
4
5
function* gen(){
//some code
}
var g=gen()
g[Symbol.iterator]()===g //true

next方法的参数

yield语句本身没有返回值,或者说总是返回undefined.next方法可以带有一个参数,该参数会被当作上一条yield语句的返回值.

1
2
3
4
5
6
7
8
9
function* f() {
for (let i = 0; true; i++) {
var reset = yield i
if (reset) { i = 0 }
}
}
var g = f()
console.log(g.next(), g.next(), g.next(true))
//{value: 0, done: false} {value: 1, done: false} {value: 1, done: false}

这是一个无限运行的Generator函数,如果next方法没有参数,那么变量reset的值一直为undefined,当next方法带有参数true时,变量reset被重置为true,因而i=-1

1
2
3
4
5
6
7
8
9
10
11
12
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args)
generatorObject.next()
return generatorObject
}
}
var wrapped = wrapper(function* () {
console.log(`First Input:${yield}`)
return 'Done'
})
wrapped().next('hello')

如果想要在第一次调用next方法就能输入值,需要在Generator函数外面再包一层.

for…of循环

for…of循环可以自动便利Generator函数生成的Iterator对象,且此时不再需要调用next方法.

1
2
3
4
5
6
7
8
9
10
11
function* foo() {
yield 1
yield 2
yield 3
yield 4
yield 5
return 6
}
for (const iterator of foo()) {
console.log(iterator)
}

用Generator函数和for…of循环实现Fibonacci数列

1
2
3
4
5
6
7
8
9
10
11
12
13
function* fibo() {
let [prev, curr] = [0, 1];
for (; ;) {
[prev, curr] = [curr, prev + curr]
yield curr
}
}
for (const n of fibo()) {
if (n > 1000) {
break
}
console.log(n);
}

利用for..of循环,为不具有Iterator接口的对象添加遍历器接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
function* objectEntries() {
let propKeys = Object.keys(this)
for (const propKey of propKeys) {
yield [propKey, this[propKey]]
}
}
let foo = { first: 'foo', last: 'bar' }
foo[Symbol.iterator] = objectEntries
for (const [k, v] of foo) {
console.log([k, v])
}
// ["first", "foo"]
// ["last", "bar"]

Generator.prototype.throw()

Generator函数返回的遍历器有一个throw方法,可以在函数体外抛出错误,然后再函数体内捕获.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* g() {
try {
yield
} catch (e) {
console.log("内部捕获", e)
}
}
var i = g()
i.next()
try {
i.throw('a')
i.throw('b')
} catch (e) {
console.log('外部捕获', e);
}

第二次抛出错误的时候,由于Generator函数内部的catch语句已经执行过了,所以不会捕捉到错误,被函数体外的catch捕获

1
2
3
4
5
6
7
8
9
10
11
12
function* foo() {
var x = yield 3
var y = x.toUpperCase()
yield y
}
var bar = foo()
bar.next()
try {
bar.next(42)
} catch (e) {
console.log(e)
}

Generator函数体内抛出的错误可以再函数体内捕获,反过来,Generator函数体内抛出的错误也可以在函数外捕获.

Generator.prototype.return()

Generator函数返回的遍历器有一个return方法,可以返回给定的值,并总结Generator函数的遍历

1
2
3
4
5
6
7
8
function* foo() {
yield 1
yield 2
yield 3
}
var bar = foo()
console.log(bar.next(), bar.return());
//{value: 1, done: false} {value: undefined, done: true}

yield*

如果要再Generator函数里调用另一个Generator函数,就要用yield*语句

1
2
3
4
5
function* foo() {
yield 1
yield 2
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* bar() {
yield 'x'
yield* foo()
yield 'y'
}
//等同于
function* bar() {
yield 'x'
yield 1
yield 2
yield 'y'
}
//等同于
function* bar() {
yield 'x'
for(let v of foo()){
yield v
}
yield 'y'
}

Generator函数的异步应用

异步任务的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
const fetch = require('node-fetch');
function* gen() {
var url = 'https://www.baidu.com'
var res = yield fetch(url)
console.log(res)
}
var g = gen()
var res = g.next()
res.value.then(data => {
return data
}).then(data => {
g.next(data)
})

Fetch模块返回的是Promise对象,要用then方法调用下一个next方法,虽然简洁,但是流程管理不便.

Thunk函数

Thunk函数是自动执行Generator函数的方法

1
2
3
4
5
6
7
8
const fs = require('fs');
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName,callback)
}
}
var readFileThunk = Thunk(fileName)
readFileThunk(callback)

任何函数,只要参数有回调函数,就能写成Thunk函数形式

Thunk函数转换

1
2
3
4
5
6
7
const Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}

一个例子

1
2
3
4
5
function f(a, callback) {
callback(a)
}
var ft = Thunk(f)
ft(1)(console.log)

Thunkify模块

1
$ npm install thunkify

使用方法

1
2
3
4
var thunkify=require('thunkify')
var fs=require('fs')
var read=thunkify(fs.readFile)
read('test.txt')(callback)

Generator函数流程管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');
const thunkify = require('thunkify');
var read = thunkify(fs.readFile)
var gen = function* () {
var r1 = yield read('./a.txt')
console.log(r1.toString())
var r2 = yield read('./b.txt')
console.log(r2.toString())
}
var g = gen()
var r1 = g.next()
r1.value((err, data) => {
if (err) { throw err }
var r2 = g.next(data)
r2.value((err, data) => {
if (err) { throw err }
g.next(data)
})
})

Generator函数的执行过程其实是将同一个回调函数反复传入next方法的value属性.可以用递归来自动完成这个过程

Thunk函数的自动流程管理

下面是一个基于Thunk函数的Generator执行器的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function run(fn) {
var gen = fn()
function next(err, data) {
var res = gen.next(data)
if (res.done) { return }
result.value(next)
}
next()
}
function* g() {
var f1 = yield readFile('A')
var f2 = yield readFile('B')
var f3 = yield readFile('C')
}
run(g)

函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成,这样一来,异步操作不仅可以写得像同步操作,而且只需要一行代码就可以执行

co模块

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');
const co = require('co');
const thunkify = require('thunkify');
var readFile = thunkify(fs.readFile)
var gen = function* () {
var f1 = yield readFile('./a.txt')
var f2 = yield readFile('./b.txt')
console.log(f1.toString())
console.log(f2.toString())
return 'Done'
}
co(gen).then((res) => {
console.log(res);
})

使用co模块无需编写Generator函数执行器,co函数返回一个Promise,可以添加回调函数

处理并发的异步操作

co支持并发的异步操作,就是允许某些操作同时进行,等到它们全部完成才进行下一步

这时要把并发的操作放在数组或者对象里,放在yield语句后面

1
2
3
4
5
6
co(function* () {
var res = yield [Promise.resolve(1), Promise.resolve(2)]
console.log(res)
}).catch(err => {
console.log(err)
})
1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');
const co = require('co');
const thunkify = require('thunkify');
var readFile = thunkify(fs.readFile)
co(function* () {
var values = [1, 2, 3]
res = yield values.map(time2)
console.log(res);
})
function time2(v) {
return v * 2
}