Appearance
Generator 函数
简介
Generator 函数是 ES6 提供的一种异步编程解决方案
执行 Generator 函数会返回一个遍历器对象。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数是一个普通函数,但有两大特征
- function 关键字与函数名之间有一个星号
- 函数体内部使用 yield 表达式,定义不同的内部状态
js
function* helloWorldGenerator() {
yield 'hello'
yield 'world'
return 'ending'
}
var hw = helloWorldGenerator()
hw.next()
hw.next()
hw.next()
/*
{value: 'hello', done: false}
{value: 'world', done: false}
{value: 'ending', done: true}
*/function 关键字与函数名之间的星号位置,没有明确规定(推荐第三种)
js
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }Generator 调用方式
调用 Generator 函数后,该函数并不执行,返回值不是函数运行结果,而是一个指向内部状态的指针对象。
需要调用 指针对象的 next 方法,使指针移向下一个状态
每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)
Generator 函数是分段执行:yield 表达式是暂停执行的标记,而 next 方法可以恢复执行
next 方法
返回值:含 value 和 done 两个属性的对象
- value:当前的内部状态的值,是 yield 表达式后面那个表达式的值
- done:是否遍历结束(布尔值)
运行逻辑
- 遇到 yield 表达式,就暂停执行后面的操作
- 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
- 没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值
- 没有 return 语句,则返回的对象的 value 属性值为 undefined
参数
next 方法:可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值
js
function* f() {
for (var i = 0; true; i++) {
var reset = yield i
if (reset) {
i = -1
}
}
}
var g = f()
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }Generator 函数从暂停状态到恢复运行,上下文状态(context)是不变的。
向 next 方法传参,可以在 Generator 函数开始运行之后,向函数体内部注入值。
js
function* foo(x) {
var y = 2 * (yield x + 1)
var z = yield y / 3
return x + y + z
}
var a = foo(5)
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5)
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }传值是把上一个 yield 表达式值替换为所传参数
next 方法参数:表示上一个 yield 表达式的返回值,所以第一次使用 next 方法时,传递参数是无效的
V8 引擎直接忽略第一次使用 next 方法时的参数
要使第一次 next 传参有效,Generator 函数外包一层
js
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args)
generatorObject.next()
return generatorObject
}
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`)
return 'DONE'
})
wrapped().next('hello!')
// First input: hello!yield 表达式
返回值:默认返回 undefined,可通过向 next 传参改变
Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,未执行前处于暂停状态。yield 表达式就是暂停标志。
yield 表达式为惰性求值
js
function* gen() {
yield 123 + 456
}当指针移向 123+456 这行时,才会求值
yield 和 return
相似点:返回紧跟在语句后面的表达式的值
区别:遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句直接结束
Generator 函数可以不用 yield 表达式,变成暂缓执行函数
js
function* f() {
console.log('执行了!')
}
var generator = f()
setTimeout(function () {
generator.next()
}, 2000)yield 只能在 Generator 函数使用,其他地方报错
js
(function (){
yield 1;
})()
// SyntaxError: Unexpected number例子:打平数组
js
var arr = [1, [[2, 3], 4], [5, 6]]
var flat = function* (a) {
var length = a.length
for (var i = 0; i < length; i++) {
var item = a[i]
if (typeof item !== 'number') {
yield* flat(item)
} else {
yield item
}
}
}
for (var f of flat(arr)) {
console.log(f)
}
// 1, 2, 3, 4, 5, 6不能使用 forEach,因为 yeild 只能在 Generator 函数中使用
yield 表达式用在另一个表达式之中,必须放在圆括号里面。
js
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号
js
function* demo() {
foo(yield 'a', yield 'b') // OK
let input = yield // OK
}与 Iterator 关系
Generator 函数是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator,让该对象具有 Iterator 接口
js
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
;[...myIterable] // [1, 2, 3]Generator 函数执行后,返回一个遍历器对象。该对象也具有 Symbol.iterator,执行后返回自身
js
function* gen() {
// some code
}
var g = gen()
g[Symbol.iterator]() === g
// truefor...of
for...of 循环:自动遍历 Generator 函数运行时生成的 Iterator 对象
next 方法返回对象 done 为 true,循环终止,并且不包含该对象
原生 JavaScript 对象遍历
直接编写遍历函数
js
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj)
for (let propKey of propKeys) {
yield [propKey, obj[propKey]]
}
}
let jane = { first: 'Jane', last: 'Doe' }
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`)
}
// first: Jane
// last: Doe遍历函数添加到 Symbol.iterator
js
jane[Symbol.iterator] = objectEntries
for (let [key, value] of jane) {
console.log(`${key}: ${value}`)
}扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
js
function* numbers() {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
;[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers()
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2Generator.prototype
throw()
throw 方法:在函数体外抛出错误,然后在 Generator 函数体内捕获。接受一个参数,该参数会被 catch 语句接收
js
var g = function* () {
try {
yield
} catch (e) {
console.log('内部捕获', e)
}
}
var i = g()
i.next()
try {
i.throw('a')
i.throw('b')
} catch (e) {
console.log('外部捕获', e)
}
// 内部捕获 a
// 外部捕获 b第一次抛出错误,通过内部捕获。第二次抛出错误,try..catch 已执行,错误被外部捕获
与全局 throw 区别:
全局 throw 抛出的错误,只能由外部捕获,不能由 Generator 内部捕获
js
var g = function* () {
while (true) {
try {
yield
} catch (e) {
if (e != 'a') throw e
console.log('内部捕获', e)
}
}
}
var i = g()
i.next()
try {
throw new Error('a')
throw new Error('b')
} catch (e) {
console.log('外部捕获', e)
}
// 外部捕获 [Error: a]Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误,将被外部 try...catch 代码块捕获
Generator 函数内部和外部,都没有部署 try...catch 代码块,那么程序将报错,直接中断执行
js
var gen = function* gen() {
yield console.log('hello')
yield console.log('world')
}
var g = gen()
g.next()
g.throw()
// hello
// Uncaught undefinedthrow 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法。等同于通过启动 Generator
js
function* gen() {
try {
yield 1
} catch (e) {
console.log('内部捕获')
}
}
var g = gen()
g.throw(1)
// Uncaught 1throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是 throw 抛出的错误,不会影响下次遍历
js
var gen = function* gen() {
try {
yield console.log('a')
} catch (e) {
// ...
}
yield console.log('b')
yield console.log('c')
}
var g = gen()
g.next() // a
g.throw() // b
g.next() // c函数内捕获错误的好处:多个 yield 表达式,可以只用一个 try...catch 代码块来捕获错误
Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了
js
function* g() {
yield 1
console.log('throwing an exception')
throw new Error('generator broke!')
yield 2
yield 3
}
function log(generator) {
var v
console.log('starting generator')
try {
v = generator.next()
console.log('第一次运行next方法', v)
} catch (err) {
console.log('捕捉错误', v)
}
try {
v = generator.next()
console.log('第二次运行next方法', v)
} catch (err) {
console.log('捕捉错误', v)
}
try {
v = generator.next()
console.log('第三次运行next方法', v)
} catch (err) {
console.log('捕捉错误', v)
}
console.log('caller done')
}
log(g())
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller donereturn()
return()方法:返回给定的值,并且终结遍历 Generator 函数
js
function* gen() {
yield 1
yield 2
yield 3
}
var g = gen()
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }不提供参数,则返回值的 value 属性为 undefined
Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return()方法会导致立刻进入 finally 代码块
js
function* numbers() {
yield 1
try {
yield 2
yield 3
} finally {
yield 4
yield 5
}
yield 6
}
var g = numbers()
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }调用return()方法后,就开始执行finally代码块,不执行try里面剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回值
next、throw、return
next、throw、return 作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式
next:将 yield 表达式替换成一个值
js
const g = function* (x, y) {
let result = yield x + y
return result
}
const gen = g(1, 2)
gen.next() // Object {value: 3, done: false}
gen.next(1) // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;throw:将 yield 表达式替换成一个 throw 语句
js
gen.throw(new Error('出错了')) // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));return:将 yield 表达式替换成一个 return 语句
js
gen.return(2) // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;yield * 表达式
在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历
js
function* foo() {
yield 'a'
yield 'b'
}
function* bar() {
yield 'x'
// 手动遍历 foo()
for (let i of foo()) {
console.log(i)
}
yield 'y'
}
for (let v of bar()) {
console.log(v)
}
// x
// a
// b
// yES6 提供了 yield * 表达式,简化上述操作
js
function* foo() {
yield 'a'
yield 'b'
}
function* bar() {
yield 'x'
yield* foo()
yield 'y'
}
for (let v of bar()) {
console.log(v)
}
// x
// a
// b
// y从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象
js
let delegatedIterator = (function* () {
yield 'Hello!'
yield 'Bye!'
})()
let delegatingIterator = (function* () {
yield 'Greetings!'
yield* delegatedIterator
yield 'Ok, bye.'
})()
for (let value of delegatingIterator) {
console.log(value)
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."yield 后面 Generator 函数中
- 无 return 语句,for...of 简写形式
- 有 return 语句,用
var value = yield* iterator取值
任何数据结构只要有 Iterator 接口,就可以被yield*遍历
一个例子
js
function* genFuncWithReturn() {
yield 'a'
yield 'b'
return 'The result'
}
function* logReturned(genObj) {
let result = yield* genObj
console.log(result)
}
;[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]存在两次遍历
- 扩展运算符遍历函数 logReturned 返回的遍历器对象
yield*语句遍历函数 genFuncWithReturn 返回的遍历器对象
例子:取出嵌套数组的所有成员(打平数组)
js
function* iterTree(tree) {
if (Array.isArray(tree)) {
for (let i = 0; i < tree.length; i++) {
yield* iterTree(tree[i])
}
} else {
yield tree
}
}
const tree = ['a', ['b', 'c'], ['d', 'e']]
for (let x of iterTree(tree)) {
console.log(x)
}
// a
// b
// c
// d
// e例子:yield* 语句遍历完全二叉树
js
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left
this.label = label
this.right = right
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left)
yield t.label
yield* inorder(t.right)
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null)
return new Tree(make(array[0]), array[1], make(array[2]))
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]])
// 遍历二叉树
var result = []
for (let node of inorder(tree)) {
result.push(node)
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']作为对象属性的 Generator
js
let obj = {
* myGeneratorMethod() {
···
}
};
// 等效写法
let obj = {
myGeneratorMethod: function* () {
// ···
}
};Generator 中 this
Generator 函数总是返回一个遍历器。ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法
js
function* g() {}
g.prototype.hello = function () {
return 'hi!'
}
let obj = g()
obj instanceof g // true
obj.hello() // 'hi!'返回的是遍历器对象,而不是 this 对象
js
function* g() {
this.a = 11
}
let obj = g()
obj.next()
obj.a // undefinedGenerator 不能和 new 命令一起使用
js
function* F() {
yield (this.x = 2)
yield (this.y = 3)
}
new F()功能:Generator 函数返回一个正常的对象实例,可以用 next 方法,又可以获得 this
实现:生成一个空对象,使用 call 方法绑定 Generator 函数内部的 this
js
function* F() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
var obj = {}
var f = F.call(obj)
f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3F.call 将 this 指向 obj,obj 可以等效于 F 的实例对象
问题:实例对象是 obj,遍历对象为 f,两者分离了。
解决:将 obj 换成 F.prototype
js
function* F() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
var f = F.call(F.prototype)
f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3改造为构造函数,可以使用 new 命令
js
function* F() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
function Fun() {
return F.call(F.prototype)
}
const f = new Fun()
f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3含义
Generator 和 状态机
Generator 是实现状态机的最佳结构
js
var ticking = true
var clock = function () {
if (ticking) console.log('Tick!')
else console.log('Tock!')
ticking = !ticking
}js
var clock = function* () {
while (true) {
console.log('Tick!')
yield
console.log('Tock!')
yield
}
}Generator 实现与 ES5 实现对比
- 更简洁,更安全(状态不会被非法篡改)
- 更符合函数式编程的思想
Generator 和 协程
协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。
协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。
协程与子例程的差异
传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数
协程特点
并行执行:多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended)
交换执行权:一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。
这种可以并行执行、交换执行权的线程(或函数),就称为协程
从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
协程作用
JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
协程与普通线程的差异
与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。 协程适合用于多任务运行的环境。
不同之处在于
协程:合作式。执行权由协程自己分配。 普通线程:抢先式。哪个线程优先得到资源,必须由运行环境决定
Generator 和 协程关系
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,使用 yield 表达式交换控制权
Generator 上下文
JavaScript 上下文:JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。执行函数(或块级代码),又会形成函数上下文,形成上下文堆栈。这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 上下文:Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行
应用
Generator 可以暂停函数执行,返回任意表达式的值。
异步操作的同步化表达
Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数
js
function* loadUI() {
showLoadingScreen()
yield loadUIDataAsynchronously()
hideLoadingScreen()
}
var loader = loadUI()
// 加载UI
loader.next()
// 卸载UI
loader.next()第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用 next 方法,则会隐藏 Loading 界面。
示例:Generator 函数逐行读取文本文件
js
function* numbers() {
let file = new FileReader('numbers.txt')
try {
while (!file.eof) {
yield parseInt(file.readLine(), 10)
}
} finally {
file.close()
}
}控制流管理
场景:多步操作非常耗时,采用回调函数
js
step1(function (value1) {
step2(value1, function (value2) {
step3(value2, function (value3) {
step4(value3, function (value4) {
// Do something with value4
})
})
})
})采用 Promise 改写
js
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(
function (value4) {
// Do something with value4
},
function (error) {
// Handle any error from step1 through step4
}
)
.done()Generator 函数改写
js
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1)
var value3 = yield step2(value2)
var value4 = yield step3(value3)
var value5 = yield step4(value4)
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}使用 一个 函数,按次序自动执行所有步骤
js
scheduler(longRunningTask(initialValue))
function scheduler(task) {
var taskObj = task.next(task.value)
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task)
}
}只适合同步操作,因为得到返回值就向下运行,未考虑实际运行情况
利用 for...of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法
steps 将一个任务分为多个步骤
js
let steps = [step1Func, step2Func, step3Func]
function* iterateSteps(steps) {
for (var i = 0; i < steps.length; i++) {
var step = steps[i]
yield step()
}
}jobs 将项目分解为多个任务
js
let jobs = [job1, job2, job3]
function* iterateJobs(jobs) {
for (var i = 0; i < jobs.length; i++) {
var job = jobs[i]
yield* iterateSteps(job.steps)
}
}执行所有任务的所有步骤
js
for (var step of iterateJobs(jobs)) {
console.log(step.id)
}for...of 的本质是一个 while 循环
js
var it = iterateJobs(jobs)
var res = it.next()
while (!res.done) {
var result = res.value
// ...
res = it.next()
}部署 Iterator 接口
利用 Generator 函数,可以在任意对象上部署 Iterator 接口
js
function* iterEntries(obj) {
let keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
yield [key, obj[key]]
}
}
let myObj = { foo: 3, bar: 7 }
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value)
}
// foo 3
// bar 7作为数据结构
Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口
js
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt')
yield fs.readFile.bind(null, 'world.txt')
yield fs.readFile.bind(null, 'and-such.txt')
}js
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}ES5 用数组模拟 Generator
js
function doStuff() {
return [fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt')]
}