javascript 老腔常谈之 generator的黑魔法

    科技2025-08-27  14

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

    Processed: 0.015, SQL: 8