异步发展流程 —— Generators + co 让异步更优雅
栏目: JavaScript · 发布时间: 6年前
内容简介:Generator 函数是 ES6 提供的一种异步编程解决方案,是一个生成器,用于生成一个遍历器的函数,语法行为与传统函数完全不同。JavaScript 原有的表示 “集合” 的数据结构,主要有遍历器 Iterator 就是这样一种机制,它是一种接口,为不同的数据结构提供统一的、简便的访问机制,任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作,即依次处理该数据结构的所有成员。
Generator 函数是 ES6 提供的一种异步编程解决方案,是一个生成器,用于生成一个遍历器的函数,语法行为与传统函数完全不同。
Iterator 遍历器
JavaScript 原有的表示 “集合” 的数据结构,主要有 Array
和 Object
,在 ES6 中又加入了 Set
和 Map
,这样就有了四种数据集合,还可以组合使用它们,如数组的成员是 Map
或 Object
,这样就需要一种统一的接口机制,用来处理所有不同的数据结构。
遍历器 Iterator 就是这样一种机制,它是一种接口,为不同的数据结构提供统一的、简便的访问机制,任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作,即依次处理该数据结构的所有成员。
Iterator 遍历器其实就是一个指针对象,上面有 next
方法,第一次调用 next
指针指向数据结构的第一个成员,第二次 next
调用指针指向第二个成员,直到指针指向最后一个成员。
我们可以使用 ES6 的展开运算符 ...
和 for...of...
去遍历带有 Iterator 接口的数据结构,需要注意的是, Object
本身不具备 Iterator 接口,所以我们无法通过 ...
把一个对象扩展到一个数组中,并且会报错,我们可以通过代码手动将 Object
类型实现 Iterator 接口。
// 通过 Generator 函数给 Object 扩展 Iterator 接口 Object.prototype[Symbol.iterator] = function*() { for (var key in this) { yield this[key]; } }; // 测试 Iterator 接口 let obj = { a: 1, b: 2, c: 3 }; let arr = [...obj]; console.log(arr); // [1, 2, 3] 复制代码
上面我们其实是通过 ES6 的 Generator 函数简单粗暴的给 Object
类型实现了 Iterator 接口,后面我们会简单模拟 Generator 生成器。
模拟 Generator
Generator 函数是一个生成器,调用后会返回给我们一个 Iterator 遍历器对象,在对象中有一个 next
方法,调用一次 next
,帮我遍历一次,返回值为一个对象,内部有 value
和 done
两个属性, value
属性代表当前遍历的值, done
属性代表是否遍历完成,如果遍历完成后继续调用 next
,返回的对象中 value
属性值为 undefined
, done
属性值为 true
,这个遍历器在进行数据遍历时更像给我们提供了一个暂停功能,每次都需要手动调用 next
去进行下一次遍历。
我们根据 Generator 的特性用 ES5 简单模拟一个遍历器生成函数:
// 模拟遍历器生成函数 function iterator(arr) { var i = 0; return { next: function() { var done = i >= arr.length; var value = !done ? arr[i++] : undefined; return { value: value, done: done }; } }; } 复制代码
测试一下模拟的遍历器生成函数:
// 测试 iterator 函数 var arr = [1, 3, 5]; // 遍历器 var result = iterator(arr); result.next(); // {value: 1, done: false} result.next(); // {value: 3, done: false} result.next(); // {value: 5, done: false} result.next(); // {value: undefined, done: true} 复制代码
Generator 的基本使用
在普通的函数 function
关键字后加一个 *
就代表声明了一个生成器函数,执行后返回一个遍历器对象,每次调用遍历器的 next
方法时,遇到 yield
关键字暂停执行,并将 yield
关键字后面的值会作为返回对象中 value
的值,如果函数有返回值,会把返回值作为调用 next
方法进行遍历完成后返回的对象中 value
的值,果已经遍历完成,再次 next
调用这个 value
的值会变成 undefined
。
// 生成器函数 function* gen() { yield 1; yield 2; return 3; } // 遍历器 let it = gen(); it.next(); // {value: 1, done: false} it.next(); // {value: 2, done: false} it.next(); // {value: 3, done: true} it.next(); // {value: undefined, done: true} 复制代码
在 Generator 函数中可以使用变量接收 yield
关键字执行后的返回值,只是接收的值并不是 yield
关键字后面表达式执行的结果,而是遍历器在下一次调用 next
方法时传入的参数。
也就是说我们第一次调用 next
方法进行遍历时是不需要传递参数的,因为上面并没有变量来接收它,即使传参也会被忽略掉,我们用一个例子感受一下这种比较特殊的执行机制:
// 生成器函数 function* gen(arr) { let a = yield 1; let b = yield a; let c = yield b; return c; } // 遍历器 let it = gen(); it.next(); // {value: 1, done: false} it.next(2); // {value: 2, done: false} it.next(3); // {value: 3, done: false} it.next(4); // {value: 4, done: true} it.next(5); // {value: undefined, done: true} 复制代码
如果已经遍历完成,并把上次遍历接收到的值作为返回值传递给返回对象 value
属性的值,后面再次调用 next
传入的参数也会被忽略,返回对象的 value
值为 undefined
。
在 Generator 函数中,如果在其他函数或方法调用的回调内部(函数的执行上/下文发生变化)不能直接使用 yield
关键字。
// 错误的写法 function* gen(arr) { arr.forEach(*item => { yield* item; }); } // 正确的写法 function* gen(arr) { for(let i = 0; i < arr.length; i++) { yield arr[i]; } } 复制代码
如果在一个 Generator 函数中调用了另一个 Generator 函数,在调用外层函数返回遍历器的 next
方法时是不会遍历内部函数返回的遍历器的。
// 外层的生成器函数 function* genOut() { yield "a"; yield genIn(); yield "c"; } // 内层的生成器函数 function* genIn() { yield "b"; } // 遍历器 let it = genOut(); it.next(); // {value: 'a', done: false} it.next(); // 返回 genIn 的遍历器对象 it.next(); // {value: 'c', done: false} it.next(); // {value: undefined, done: true} 复制代码
上面代码如果想在调用 genOut
返遍历器的 next
方法时,同时遍历 genIn
调用后返回的遍历器,需要使用 yield*
表达式。
// 外层的生成器函数 function* genOut() { yield "a"; yield* genIn(); yield "c"; } // 内层的生成器函数 function* genIn() { yield "b"; } // 遍历器 let it = genOut(); it.next(); // {value: 'a', done: false} it.next(); // {value: 'b', done: false} it.next(); // {value: 'c', done: false} it.next(); // {value: undefined, done: true} 复制代码
在 genOut
返回的遍历器调用 next
遇到 yield*
表达式时帮我们去遍历了 genIn
返回的遍历器,其实 yield*
内部做了处理,等同于下面代码:
// 外层的生成器 function* genOut() { yield "a"; for (let v of genIn()) { yield v; } yield "c"; } // 内层的生成器 function* genIn() { yield "b"; } // 遍历器 let it = genOut(); it.next(); // {value: 'a', done: false} it.next(); // {value: 'b', done: false} it.next(); // {value: 'c', done: false} it.next(); // {value: undefined, done: true} 复制代码
Generators 与 Promise 结合
Promise 也是 ES6 的规范,同样是解决异步的一种手段,如果对 Promise 还不了解,可以阅读下面两篇文章:
因为 Generator 函数在执行时遇到 yield
关键字会暂停执行,那么 yield
后面可以是异步操作的代码,比如 Promise,需要继续执行,就手动调用返回遍历器的 next
方法,因为中间有一个等待的过程,所以在执行异步代码的时候避免了回调函数的嵌套,在写法上更像同步,更容易理解。
我们来设计一个 Generator 函数与 Promise 异步操作结合的使用场景,假设我们需要使用 NodeJS 的 fs
模块读取一个文件 a.txt
的内容,而 a.txt
的内容是另一个需要读取文件 b.txt
的文件名,读取 b.txt
最后打印读取到的内容 “Hello world”。
回调函数的实现:
// 引入依赖 let fs = require("fs"); fs.readFile("a.txt", "utf8", (err, data) => { if (!err) { fs.readFile(data, "utf8", (err, data) => { if (!err) { console.log(data); // Hello world } }); } }); 复制代码
上面代码因为只有两层回调函数嵌套,所以感觉没那么复杂,但是嵌套的回调函数多了,代码就不那么的优雅了,我们接下来使用 Generator 结合 Promise 来实现,为了方便将 fs
异步的方法转换成 Promise,我们引入 util
模块,并转换 readFile
方法。
// 引入依赖 let fs = require("fs"); let util = require("util"); // 将 readFile 方法转换成 Promise let read = util.promisify(fs.readFile); // 生成器函数 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 遍历器 let it = gen(); it.next().value.then(data => { it.next(data).then(data => { console.log(data); // Hello world }); }); 复制代码
我们只看 Generator 函数 gen
内部的执行,虽然是异步操作,但是在写法上几乎和同步没有区别,理解起来更容易,唯一美中不足的是,我们需要自己手动的调用遍历器的 next
和 Promise 实例的 then
,这个问题 co
库可以帮我们解决。
co 库的使用
co 库的作者是著名的 NodeJS 大神 TJ,是基于 Generator 和 Promise 的,这个库能帮我们实现自动调用 Generator 函数返回遍历器的 next
方法,并执行 yield
后面 Promise 实例的 then
方法,所以每次 yield
后面的异步操作返回的必须是一个 Promise 实例,代码看起来像同步,执行其实是异步,不用自己手动进行下一次遍历,这更是我们想要的。
由于 co 是一个第三方的模块,所以在使用时需要我们提前下载:
npm install co
我们使用 co
来实现之前异步连续读文件的案例:
// 引入依赖 let fs = require("fs"); let util = require("util"); let co = require("co"); // 将 readFile 方法转换成 Promise let read = util.promisify(fs.readFile); // 生成器函数 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 使用 co 库代替手动调用 next co(gen()).then(data => { console.log(data); // Hello world }); 复制代码
从上面代码可以看出, co
库的 co
函数参数是一个遍历器,即 Generator 函数执行后的返回结果,在 co
内部操作遍历器并遍历完成后返回了一个 Promise 实例,遍历器最终的返回结果的 value
值作为 then
方法回调的参数,所以我们可以使用 then
对结果进行后续的处理。
co 库的实现原理
我们其实在上面使用 co
的过程中对于 co
函数的内部做了什么已经有所了解,主要就是帮助我们调用遍历器的 next
和调用 yield
后面代码执行后返回 Promise 实例的 then
,并在整个遍历结束后,返回一个新的 Promise 实例。
下面我们根据上面分析的 co
函数的原理来模拟一个简易版的 co
库:
// myCo.js 文件 // co 函数,it 为遍历器对象 function co(it) { // 返回 Promise 实例 return new Promise((resolve, reject) => { // 异步递归 function next(data) { // 第一次调用 next 不需要传参 let { value, done } = it.next(data); if (!done) { // 如果没完成遍历,调用返回 Promise 的 then 方法 value.then(data => { // 如果 Promise 成功,继续递归,如果失败直接执行 reject next(data); }, reject); } else { // 如果遍历完成直接执行 resolve 并传入 value resolve(value); } } next(); }); } // 导出模块 module.exports = co; 复制代码
验证 myCo.js
实现的 co
函数:
// 引入依赖 let fs = require("fs"); let util = require("util"); let myCo = require("./myCo"); // 将 readFile 方法转换成 Promise let read = util.promisify(fs.readFile); // 生成器函数 function* gen() { let aData = yield read("1.txt", "utf8"); let bData = yield read(aData, "utf8"); return bData; } // 使用 co 库代替手动调用 next myCo(gen()).then(data => { console.log(data); // Hello world }); 复制代码
我们将引入的 co
库替换成了自己实现的简易版 myCo
模块,上面读取文件的案例依然生效,这说明我们模拟的 co
库核心逻辑是没问题的,跟原版不同的是并没有处理很多细节,并定义指针,如果对 co
库感兴趣建议看看 TJ 大神的源码,整个库写的非常精简,值得学习。
总结
Generators 相当于把一个函数拆分成若干个部分执行,执行一次时将指针指向下一段要执行的代码,直到结束位置,Generators 配合 co
库的使用场景多在 NodeJS 当中,并在 Koa 1.x
版本中居多,现在已经升级到 Koa 2.x
版本,使用更多的是基于 Generators 和 co
库衍生出来的 ES7 新标准 async/await
,我们在下一篇异步发展流程系列的文章中来详细介绍。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 复杂Vue组件的异步流程分析
- Node.js 异步流程控制模式
- 架构设计:异步处理流程,多种实现模式详解
- JS常用的几种异步流程控制
- JS 异步发展流程(回调函数=>Async/await)
- SpringBoot | :异步开发之异步调用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。