Generator 生成器
前言
其实关于js generator的内容算是一个老话题了,在日常的应用开发中,我们多少通过间接或直接的方式有过接触。
直接的比如,dva.js、koa.js(1.x版本)等等。
间接的包括async,await相关特性的使用。
既然是老腔常谈,我们来看一下官方对Generator的简要说明:
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g)
// 输出结果 Generator {}
Generator 实例方法:
// 返回一个由 yield表达式生成的值。
Generator.prototype.next()
// 返回给定的值并结束生成器。
Generator.prototype.return()
// 向生成器抛出一个错误。
Generator.prototype.throw()
官方例子(无限迭代器,稍作修改)
一个无限迭代器
function* idMaker(){
let index = 0;
while(true)
yield index++;
}
let gen = idMaker(); // "Generator { }"
gen.next().value
// 0
gen.next().value
// 1
gen.next().value
// 2
gen.return('complete')
// 强制结束迭代输出 'complete'
gen.next().value
// undefined
在浏览器控制台打印 Generator 实例,通过展开对象后我们可以看见下面内容:
图中我们除了原型链上看到,Generator 的实例方法外。
[[GeneratorState]]: 'suspended'
这一行作为 Generator 的私有属性,从字面意思上来看,就是状态的意思,那么我们可以认为 Generator 是一个状态机。
通过验证 Generator 有两个状态 suspended 等待、closed 关闭。Generator 的初始状态为 suspended,每次调用next方法会检测function *() {} 里面的每一行代码,如果遇到yield 就算是完成一次迭代,并返回对应的值,状态保持suspended。如果检测到return同样返回对应的值,并将状态改为closed。之后还有yield 没有执行,即使再次调用next的方法,也不再有效。Generator 的return方法同样有结束迭代的效果。
提示:Generator的状态是不可逆的,一旦状态变为closed就代表既定事实,无法恢复。
虽然我们不能通过获取Generator私有的状态,但是我们可以通next()得到的值的done属性来判断当状态,当done = true的时候就是结束或者关闭状态。
进入正题
关于异步转同步
来看一段代码
const fn = function *() {
yield new Promise(r => setTimeout(() => r('first, 2000'),2000))
yield new Promise(r => setTimeout(() => r('second, 1000'),1000))
}
const g = fn()
g.next().value.then(v => console.log(v))
g.next().value.then(v => console.log(v))
// 输出结果
// second, 1000
// first, 2000
这是一个简单利用 Generator 来进行的异步调用。
不过从输出的结果来看,并不是按我希望的按照迭代顺序来输出结果的。
来把代码稍作修改:
const g = fn()
g.next().value.then(v => {
console.log(v)
g.next().value.then(v => console.log(v))
})
// 输出结果
// first, 2000
// second, 1000
从修改后的代码能够看到,达到了预期的结果。
但是,问题来了。不光代码就传统的异步调用的代码更多,而且并没有有效解决回调地狱的问题,而且一旦异步调用的规模为n不确定时,这种手动嵌套的方式,显然是不可取的。
我们再来看一个Generator的特性:
const fn = function *() {
const first = yield 'first'
console.log(first)
const second = yield 'second'
console.log(second)
}
const g = fn()
let tempValue = g.next()
tempValue = g.next(tempValue.value)
g.next(tempValue.value)
// 输出结果
// first
// second
通过输出的结果来看,结果是从function 中完美输出。
如果只看function 这部分代码是不是有点眼熟。
const fn = async function () {
const first = await Promise.resolve('first')
console.log(first)
const second = await Promise.resolve('second')
console.log(second)
}
fn()
// 输出结果
// first
// second
其实async与await就是*与yield的语法糖。
那么问题来了,该怎么实现上述的这种自然同步效果呢。
四行代码的黑魔法
继续看代码:
// 这里致敬co.js
const Co = generator => {
// 逻辑
}
// 我们只需要将之前的代码放在里面
Co(function *() {
const first = yield new Promise(r => setTimeout(() => r('first, 2000'),2000))
console.log(first)
const second = yield new Promise(r => setTimeout(() => r('second, 1000'),1000))
console.log(second)
})
// 希望输出的结果
// first
// second
Co方法里面主要用来处理 Generator 的迭代逻辑,也就是用来干脏活。
正如标题所诉为了达到前面我们需要的结果,我们需要4行代码。对,就是这么简单!
// 如题,刚好四行,完美!
const Co = generator => {
const ge = generator()
// 这里用了一个递归来解决问题规模未知的情况
const recursion = next => {
!next.done && next.value.then(res => recursion(ge.next(res)))
}
recursion(ge.next())
}
后记
Generator 在加入JS大家庭的时候,正直整个前端高速发展的时期,那个时候涌现出了很多有意思的工具。大部分的人都沉侵在工具的选择问题上。随着后来async,await的加入,自然是大浪淘沙。不过在闲暇之余,我们同样可以学习这些尘封在历史中点滴,来提升自己审视与解决问题的能力。