一、generator简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* Hello() { // 定义generator时,需要使用function*
yield 100
yield (function () {return 200})()
return 300
}

var h = Hello() // 执行Hello()之后,Hello()内部的代码不会立即执行,而是处于一个暂停状态
console.log(typeof h) // 返回object。 执行var h = Hello()生成一个Generator对象,经验证typeof h发现generator并不是函数,后面会发现它是一个iterator

// 执行第一个h.next()时,会激活刚才的暂停状态,开始执行Hello内部的语句,但是,直到遇到yield语句。一旦遇到yield语句时,它就会将yield后面的表达式执行,并返回执行的结果,然后又立即进入暂停状态。
// 返回值中的 done: false 表示目前处于暂停状态,尚未执行结束,还可以再继续往下执行。
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如何处理异步操作

1. 通过Promise读取多个文件的方式:

1
2
3
4
5
6
7
8
9
10
11
12
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 个文件内容
})

2. 通过generator实现:

1
2
3
4
5
6
7
8
9
10
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是什么?

1. Iterator遍历器是ES6引入的,它是一个指针对象, 实现类似于单项链表的数据结构,通过next()将指针指向下一个节点。

在 ES6 中,原生具有[Symbol.iterator]属性数据类型有:数组、某些类似数组的对象(如arguments、NodeList)、Set和Map。

2. Symbol.iterator是个函数

1
2
3
4
5
6
Array.prototype[Symbol.iterator] // 返回的是一个function, 和 slice等数组方法一样。
console.log(Array.prototype.slice) // [Function: slice]
console.log(Array.prototype[Symbol.iterator]) // [Function: values]

// 比如:
console.log([1, 2, 3][Symbol.iterator]) // function values() { [native code] }

3. 具有[Symbol.iterator]属性的对象,都可以一键生成一个Iterator对象。

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

4. 遍历Iterator对象有两种方式:next 和 for…of

1
2
3
4
5
6
7
8
9
10
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 }

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

5. Generator返回的也是Iterator对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 所以:
function* Hello() {
yield 100
yield (function () {return 200})()
return 300
}
const h = Hello()
console.log(h[Symbol.iterator]) // [Function: [Symbol.iterator]]

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的基本用法

1. next向yield传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
}

// 要想看懂需谨记:next遇到yield函数暂停然后return,下一次next时再接着上一次yield之后继续执行
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

2. yield* 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 问题:想要输出a x y b,怎么做?
function* G1() {
yield 'a'
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}

// 解决
function* G1() {
yield 'a'
yield* G2() // 使用 yield* 执行 G2()
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}

// 此处使用 for...of 而不是 next
for (let item of G1()) {
console.log(item)
}

yield后面接一个普通的 JS 对象,而yield* 后面会接一个Generator。

五、generator异步实例

1. 封装Thunk 函数(其实就是函数柯里化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 读取文件的函数
fs.readFile('data1.json', 'utf-8', (err, data) => {
// 获取文件内容
})

// 将其封装成一个Thunk函数
const thunk = function (fileName, codeType) {
// 返回一个只接受 callback 参数的函数称为Thun函数
return function (callback) {
fs.readFile(fileName, codeType, callback)
}
}
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
// 获取文件内容
})

我们经过对传统的异步操作函数进行封装,得到一个只有一个参数的函数,而且这个参数是一个callback函数,那这就是一个thunk函数。当然,可以使用第三方库生成thunk函数:thunkify

1
2
3
4
5
6
// npm i thunkify --save
const thunk = thunkify(fs.readFile)
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
// 获取文件内容
})

2. generator异步

(1)在generator中使用thunk函数

1
2
3
4
5
6
7
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)
}

(2)挨个读取两个文件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
})
})

(3)优化2,实现自驱动流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 自动流程管理的函数
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)

(4)使用co库实现自助流程管理

1
2
3
4
5
6
7
8
9
// 定义 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)

六、generator的应用:Koa

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

我们此前说过,既然是处理 http 请求,是一种网络操作,肯定就会用到异步操作。express 使用的异步操作是传统的callback,而 koa 用的是我们刚刚讲的Generator(koa v1.x用的是Generator,已经被广泛使用,而 koa v2.x用到了 ES7 中的async-await)

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

2、koa 中如何应用Generator

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

七、生成器的原理

其实,在生成器generator内部,如果遇到 yield 关键字,那么 V8 引擎将返回关键字后面的内容给外部,并暂停该生成器函数的执行。生成器暂停执行后,外部的代码便开始执行,外部代码如果想要恢复生成器的执行,可以使用 result.next 方法。

那 V8 是怎么实现生成器函数的暂停执行和恢复执行的呢?

它用到的就是协程,协程是—种比线程更加轻量级的存在。我们可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行; 同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

八、总结

1、定义generator时,需要使用function*。generator返回的不是函数而是一个iterator。

2、iterator有两种遍历方式:next和for…of。next遇到yield函数暂停然后return。

3、V8引擎实现yield机制:它用到的就是协程(协程是—种比线程更加轻量级的存在),比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行。