Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

yield 与生成器:把多步埋点收进同一个函数结构里

作者:oasis


yield 关键字的出现,主要是为了解决异步编程、惰性求值和自定义迭代器这三大需求。它的设计灵感来源于其他语言(如 Python、C#),并在 ES6(ECMAScript 2015)中被正式引入 JavaScript。

惰性求值可以应用于埋点数据上报。普通函数一次调用只有一次返回,若在函数体多个分支里各写一遍 report(...),逻辑没错,但「上报」分散在条件分支里,读起来不够聚气:你想一眼看出这个入口做了哪些上报,往往要扫完整段实现。

yield 天然对应惰性求值,依托迭代器协议和生成器函数,可以让执行在函数内部多次暂停、每次交出一份「待上报」数据——从结构上看,就像一个函数具备多处有序的「交出上报项」的能力。

处理数据上报的常见写法是:封装一个函数,参数里带上要上报的信息。如下例:

js

const handleClick = (e) => {
    if(user.isAdmin) {
        report({type: 'admin'})
    } else {
        report({type: 'user'})
    }
}

<div onClick={handleClick}></div>

在上例中,通过两次调用 report 完成上报。若希望从结构上更醒目(一眼能识别「这里嵌了上报流水线」),常见做法有几类:

  1. report 封装业务方法(高阶函数)
js
function report(fn) {
    return () => {
        console.log('上报流程')
        fn()
        console.log('上报流程')
    }
}
  1. 装饰器语法(ES 或 TypeScript)
js

function report(target, name, descriptor) {
    const original = descriptor.value
    descriptor.value = function(...args) {
        console.log('上报流程')
        original.apply(this, args)
        console.log('上报流程')
    }
    return descriptor
}

class Controller {
    @report
    click() {

    }
}
  1. Proxy(ES)
js
function withReport(fn) {
    return new Proxy(fn, {
        apply(target, thisArg, args) {
            console.log('上报流程')
            const result = Reflect.apply(target, thisArg, args)
            console.log('上报流程')
            return result;
        }
    })
}

这三种方式都能从外层看出「这个函数夹着上报」,但它们通常只在被包裹函数执行的前后各插一段逻辑;真正的痛点是:函数体内部若要在多个分支、多个步骤里各报一次,仍希望结构紧凑、顺序可读,仅靠「包一层」就不够了。

这时仍可封装高阶函数,专门驱动生成器函数:在每次 yield 交值时统一走上报逻辑。

js
function report(fn) {
    return async function () {
        const gen = fn()
        let next = await gen.next()
        do {
            // 用 console.log 模拟上报逻辑
            console.log(next.value)
            
            next = await gen.next()
        } while(!next.done)
    }
}

用法示例:

js

async function * handleClick() {
    if(user.isAdmin) yield {type: 'admin'}
    else yield {type: 'user'}
}

report(handleClick)()

借助生成器函数与 yield 暂停执行,可以在一个业务函数里,用多条 yield 把「分步上报」排成显式序列;外层再套一层驱动器,上报行为便既集中又可读。