Generator Function 是 ES6 提供的一种异步流程控制解决方案。在此以前异步编程形式有,回调函数、事件监听、发布/订阅、Promise 等。但仔细思考前面解决方案,实际仍是以回调函数做为基础,并无从语法结构来改变异步写法。javascript
区别于普通函数,Generator Function 能够在执行时暂停,后面又能从暂停处继续执行。一般在异步操做时交出函数执行权,完成后在同位置处恢复执行。新语法更容易在异步场景下达到以同步形式处理异步任务。前端
以前有写过关于 Promise 解决方式和内部原理实现。接续上文,此篇文章主要阐述 迭代器相关、Generator Function 语法、yield操做符、异步场景使用、经常使用自动执行器、Babel转译等。java
注意后文将 Generator Function 翻译为生成器函数,个别处简述生成器。node
在了解生成器函数前,有必要先认识下迭代器。迭代器是一种特殊对象,具备专门为迭代流程设计的 next()
方法。每次调用 next()
都会返回一个包含 value
和 done
属性的对象。ECMAScript 文档 The IteratorResult Interface 解释为:git
简单用 ECMAScript 5 语法建立一个符合迭代器接口示例:es6
function createIterator (items) {
var i = 0
return {
next: function () {
var done = (i >= items.length)
var value = !done ? items[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = createIterator([1, 2])
console.log(iterator.next()) // {done: false, value: 1}
console.log(iterator.next()) // {done: false, value: 2}
console.log(iterator.next()) // {done: true, value: undefined}
复制代码
一般标准的 for 循环代码,使用变量 i 或 j 等来标示内部索引,每次迭代自增自减维系正确索引值。对比迭代器,循环语句语法简单,可是若是要处理多个循环嵌套则须要设置跟踪多个索引变量,代码复杂度会大大增长。迭代器的出现必定程度能消除这种复杂性,减小循环中的错误。github
除此以外,迭代器提供一致的符合迭代器协议接口,能够统一可迭代对象遍历方式。例如 for...of
语句能够来迭代包含迭代器的可迭代对象(如 Array、Map、Set、String 等)。编程
生成器是一种返回迭代器的函数,经过 function 关键字后跟星号 (*) 来表示,此外函数中还须要包含新关键字 yield。将上面示例改写为生成器函数方式。json
function *createIterator (items) {
for (let i = 0; i < items.length; i++) {
yield items[i]
}
}
const iterator = createIterator([1, 2])
console.log(iterator.next()) // {done: false, value: 1}
console.log(iterator.next()) // {done: false, value: 2}
console.log(iterator.next()) // {done: true, value: undefined}
复制代码
上述代码中,经过星号 (*) 代表 createIterator
是一个生成器函数,yield 关键字用来指定调用迭代器的 next() 方法时的返回值及返回顺序。c#
调用生成器函数并不会当即执行内部语句,而是返回这个生成器的迭代器对象。迭代器首次调用 next() 方法时,其内部会执行到 yield 后的语句为止。再次调用 next() ,会从当前 yield 以后的语句继续执行,直到下一个 yield 位置暂停。
next() 返回一个包含 value 和 done 属性的对象。value 属性表示本次 yield 表达式返回值,done 表示后续是否还有 yield 语句,即生成器函数是否已经执行完毕。
生成器相关方法以下:
生成器函数继承于 Function
和 Object
,不一样于普通函数,生成器函数不能做为构造函数调用,仅是返回生成器对象。完整的生成器对象关系图所示:
yield 关键字能够用来暂停和恢复一个生成器函数。yield 后面的表达式的值返回给生成器的调用者,能够认为 yield 是基于生成器版本的 return 关键字。yield 关键字后面能够跟 任何值 或 表达式。
一旦遇到 yield 表达式,生成器的代码将被暂停运行,直到生成器的 next() 方法被调用。每次调用生成器的next()方法时,生成器都会在 yield 以后紧接着的语句继续执行。直到遇到下一个 yield 或 生成器内部抛出异常 或 到达生成器函数结尾 或 到达 return 语句中止。
注意,yield 关键字只可在生成器内部使用,在其余地方使用会致使语法错误。即便在生成器内部函数中使用也是如此。
function *createIterator (items) {
items.forEach(item => {
// 语法错误
yield item + 1
})
}
复制代码
另外,yield *
能够用于声明委托生成器,即在 Generator 函数内部调用另外一个 Generator 函数。
Generator.prototype.next()
返回一个包含属性 done 和 value 的对象,也能够接受一个参数用以向生成器传值。返回值对象包含的 done 和 value 含义与迭代器章节一致,没有可过多说道的。值得关注的是,next() 方法能够接受一个参数,这个参数会替代生成器内部上条 yield 语句的返回值。若是不传 yield 语句返回值则为 undefined。例如:
function *createIterator (items) {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let iterator = createIterator()
console.log(iterator.next()) // {value: 1, done: false}
console.log(iterator.next(4)) // {value: 6, done: false}
console.log(iterator.next()) // {value: NaN, done: false}
console.log(iterator.next()) // {value: undefined, done: true}
复制代码
有个特例,首次调用 next() 方法时不管传入什么参数都会被丢弃。由于传给 next() 方法的参数会替代上一次 yield 的返回值,而在第一次调用 next() 方法前不会执行任何 yield 语句,因此首次调用时传参是无心义的。
事实上能给迭代器内部传值的能力是很重要的。好比在异步流程中,生成器函数执行到 yield 关键字处挂起,异步操做完成后须传递当前异步值供迭代器后续流程使用。
Generator 函数能够暂停执行和恢复执行,next() 能够作函数体内外数据交换,使其能够做为异步编程的完整解决方案。以一个异步场景为例:
function *gen () {
const url = 'https://api.github.com/user/github'
const result = yield fetch(url)
console.log(result.bio)
}
复制代码
上述代码中,Generator 函数封装了一个异步请求操做。除了增长 yield 关键字外,上面代码很是像同步操做。不过运行上述代码还须要一段执行器代码。
const g = gen()
const result = g.next()
result.value.then(function (data) => {
g.next(data.json())
})
复制代码
执行器相关代码先执行 Generator 函数获取遍历器对象,而后使用 next() 执行异步任务的第一阶段,在 fetch 返回的 promise.then 方法中调用 next 方法执行第二阶段操做。能够看出,虽然 Generator 函数把异步操做表示得很简洁,可是流程管理却不方便,须要额外手动添加运行时代码。
一般为了省略额外的手动流程管理,会引入自动执行函数辅助运行。假如生成器函数中 yield 关键字后所有为同步操做,很容易递归判断返回值 done 是否为 true 运行至函数结束。但更复杂的是异步操做,须要异步完成后执行迭代器 next(data) 方法,传递异步结果并恢复接下来的执行。但以何种方式在异步完成时执行 next(),须要提早约定异步操做形式。
经常使用的自动流程管理有 Thunk 函数模式 和 co 模块。co 一样能够支持 Thunk 函数 和 Promise 异步操做。在接下来解释自动流程管理模块前,先简单说道 Thunk 函数。
在 JavaScript 语言中,Thunk 函数指的是将多参数函数替换为一个只接受回调函数做为参数的单参数函数(注:这里多参数函数指的是相似 node 中异步 api 风格,callback 为最后入参)。相似于函数柯里化的转换过程,把接受多个参数变换成只接受一个单参数函数。以 node 中异步读取文件为例:
// 正常版本的 readFile(多参数)
fs.readFile(fileName, callback)
// Thunk 版本的 readFile (单参数)
const Tunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback)
}
}
const readFileThunk = readFileThunk(fileName)
readFileThunk(callback)
复制代码
其实任何函数参数中包含回调函数,都能写成 Thunk 函数形式。相似函数柯里化过程,简单的 Thunk 函数转换器以下所示。生成环境建议使用 Thunkify 模块,能够处理更多异常边界状况。
// Thunk 转换器
const Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
// 生成 fs.readFile Thunk 函数调用
const readFileThunk = Thunk(fs.readFile)
readFileThunk(fileA)(callback)
复制代码
先来介绍基于 Thunk 函数的自动流程管理,咱们约定 yield 关键字后的表达式返回只接受 callback 参数的函数,即前面讲的 Thunk 类型函数。基于 Thunk Generator 简单自动执行器以下。
function run (fn) {
var gen = fn()
function next (err, data) {
var result = gen.next(data)
if (result.done) return
result.value(next)
}
next()
}
复制代码
上述自动执行器函数,迭代器首先运行到首个 yield 表达式处,yield 表达式返回只接受参数为 callback 的函数,同时将 next() 递归方法做为 callback 入参执行。当异步处理完成回掉 callback 时恢复执行生成器函数。
另一种是基于 Promise 对象的自动执行机制。实际上 co 模块一样支持,Thunk 函数和 Promise 对象,两种模式自动流程管理。前者是将异步操做包装成 Thunk 函数,在 callback 中交回执行权,后者是将异步操做包装成 Promise 对象,在 then 函数中交回生成器执行权。
沿用上述示例,先将 fs 模块的 readFile 方法包装成 Promise 对象。
const fs = require('fs')
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) reject(err)
resolve(data)
})
})
}
复制代码
相较于 Thunk 模式在 callback 处理递归,Promise 对象的自动执行器,则是在 then 方法内调用递归处理方法。简单实现为:
function fun (gen) {
const g = gen()
function next (data) {
var result = g.next(data)
if (result.done) return result.value
result.value.then((function (data) {
next(data)
}))
}
next()
}
复制代码
翻阅 co 文档能够发现,yield 后对象支持多种形式:promises、thunks、array(promise)、objects (promise)、generators 和 generator functions。大体实现原理与上述一致,这里就不在贴 co 模块源码。更多信息能够参考 https://github.com/tj/co。
相似于 Promise,其实 Generator 也是有限状态机,翻阅 ECMAScript 文档 Properties of Generator Instances 会发现,生成器函数内部存在 undefined
、suspendedStart
、suspendedYield
、executing
、completed
五种状态。
从语意上很容易理解,伴随着生成器函数运行,内部状态发生相应变化。但具体 Generator 内部状态如何变化,这里暂时不继续写下去,会在下篇文章会结合 Generator es5 运行时源码讲解。
因为浏览器端环境表现不一致,并不能所有原生支持 Generator 函数,通常会采用 babel 插件 facebook/regenerator 进行编译成 es5 语法,作到低版本浏览器兼容。regenerator 提供 transform
和 runtime
包,分别用在 babel 转码和 运行时支持。
不一样于 Promise 对象引入 ployfill 垫片就能够运行,Generator 函数是新增的语法结构,仅仅依靠添加运行时代码是没法在低版本下运行的。Generator 编译成低版本可用大体流程为,编译阶段须要处理相应的抽象语法树(ast),生成符合运行时代码的 es5 语法结构。运行时阶段,添加 runtime 函数辅助编译后语句执行。
regenerator 网站 提供可视化操做,简单 ast 转码先后示例以下:
function *gen() {
yield 'hello world'
}
var g = gen()
console.log(g.next())
复制代码
var _marked = regeneratorRuntime.mark(gen);
function gen() {
return regeneratorRuntime.wrap(function gen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 'hello world';
case 2:
case "end":
return _context.stop();
}
}
}, _marked, this);
}
var g = gen();
console.log(g.next());
复制代码
regenerator-transform
插件处理 ast 语法结构,regenerator-runtime
提供 运行时 regeneratorRuntime 对象支持。这里涉及到 babel 如何转码以及 运行时框架如何运行,内容较多会新起一篇文章再来细说。具体源码可参考 facebook/regenerator 项目。
插个话题,说到 babel 来科普下常见 bable-polyfill
、babel-runtime
、babel-plugin-transform-runtime
插件功能与区别。babel 只负责 es 语法转换,不会对新的对象或方法进行转换,好比 Promise、Array.from 等。babel-polyfill 或 babel-runtime 能够用来模拟实现相应的对象。
babel-polyfill 和 babel-runtime 二者的区别在于,polyfill 会引入新的全局对象,修改污染掉原有全局做用域下的对象。runtime 则是将开发者依赖的全局内置对象,抽取成单独的模块,并经过模块导入的方式引入,避免对全局做用域污染。
babel-runtime 与 babel-plugin-transform-runtime 区别在于,前者是实际导入项目代码的功能模块,后者是用于构建过程的运行时代码抽取转换,将所需的运行时代码引用自 babel-runtime。
能够参考两篇文章,babel-polyfill使用与性能优化,babel-runtime使用与性能优化。
阮老师书中有提到相应的关系,能够在 Generator 函数章节查看。前端不多涉及进程、线程、协程知识点,这里就不在赘述。
可迭代协议容许 JavaScript 对象去定义它们的迭代行为, 例如在 for...of 结构中什么值能够循环。经常使用数据类型都内置了可迭代对象而且有默认的迭代行为, 好比 Array、Map, 注意 Object 默认不能使用 for...of 遍历。
为了变成可迭代对象,一个对象必须实现 @@iterator
方法, 能够在这个对象(或者原型链上的某个对象)设置 Symbol.iterator 属性,其属性值为返回一个符合迭代器协议对象的无参函数。
接着说迭代器协议,其定义了一种标准的方式来产生序列值。即迭代器对象必须实现 next()
方法且 next()
包含 done 和 value 属性。两个属性同上,前面有过详细解释。
简而言之,可迭代对象必须知足可迭代协议有 Symbol.iterator
方法, Symbol.iterator
方法返回符合迭代器协议对象,包含 next 方法。
看个示例,Object 对象默认不存在迭代器方法,不能使用 for...of 遍历。咱们能够修改 Object 原型添加迭代器方法,能够来访问相应 key、value 属性值。
Object.prototype[Symbol.iterator] = function () {
let i = 0
let done, value
const items = Object.entries(this)
return {
next: function () {
done = (i >= items.length)
value = done ? undefined : {
key: items[i][0],
value: items[i][1]
}
i += 1
return {
done: done,
value: value
}
}
}
}
const obj = {
name: 'spurs',
age: '23'
}
for (let item of obj) {
console.log(item)
}
// {key: "name", value: "spurs"}
// {key: "age", value: "23"}
复制代码
啰哩啰嗦,写了一篇入门级文章,不少地方都是走马观花一句带过。不过本篇开始也只定位在 Generator 语法入门,后续会再写篇 Generator 构建与运行时源码分析。
目前异步流程最佳解决方案已经是 async/await
组合,相比而言语义更清晰,不须要额外自动执行模块。但其本质上是 Generator 一种语法糖,更好的理解生成器函数会从根源上认识异步流程控制的发展历程。
最后,若有错误,敬请指正。
参考文档
参考书籍
欢迎关注笔者公众号