一、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 协程恢复执行。