javascript 老腔常谈之 generator的黑魔法

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的加入,自然是大浪淘沙。不过在闲暇之余,我们同样可以学习这些尘封在历史中点滴,来提升自己审视与解决问题的能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值