yield 与生成器:把多步埋点收进同一个函数结构里
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 完成上报。若希望从结构上更醒目(一眼能识别「这里嵌了上报流水线」),常见做法有几类:
- report 封装业务方法(高阶函数)
js
function report(fn) {
return () => {
console.log('上报流程')
fn()
console.log('上报流程')
}
}- 装饰器语法(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() {
}
}- 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 把「分步上报」排成显式序列;外层再套一层驱动器,上报行为便既集中又可读。