Effect.fnUntracedEager 什么时候该用?
在空函数体、runSync 循环三十万次的微基准里,Effect.fnUntracedEager 每次调用约 0.06 微秒,Effect.fnUntraced 约 0.4 微秒。但快的代价是:哪怕那个 effect 之后永远不运行,函数体的前缀副作用也已经发生了。
只在函数体前缀是纯计算、没有副作用的热路径上用 Effect.fnUntracedEager;前缀里有日志、写入、读取可变状态这类副作用时,一律用 Effect.fnUntraced。
原因在执行时机:fnUntraced 的函数体要等运行时才开始,符合「Effect 是描述」;fnUntracedEager 在调用时就立刻同步执行函数体,直到遇到第一个异步 effect 才交还给运行时。三个 Effect.fn 系函数的整体对比见 Effect.fn 和 fnUntraced 怎么选?,Effect 为什么是描述而不是过程,见 Effect 和 Promise 有什么区别?。
fnUntracedEager 和 fnUntraced 的执行时机差在哪?
下面用一个日志数组记录函数体什么时候开始:
import { Effect } from "effect"
const log: Array<string> = []
const lazy = Effect.fnUntraced(function*(n: number) {
log.push(`lazy body ${n}`)
yield* Effect.sync(() => log.push("lazy step"))
return n
})
const eager = Effect.fnUntracedEager(function*(n: number) {
log.push(`eager body ${n}`)
yield* Effect.sync(() => log.push("eager step"))
return n
})
const l = lazy(1)
log.push("after lazy call")
const e = eager(1)
log.push("after eager call")
Effect.runSync(l)
Effect.runSync(e)
console.log(log)输出:
[
'after lazy call',
'eager body 1',
'after eager call',
'lazy body 1',
'lazy step',
'eager step'
]lazy 的函数体在 runSync 之后才开始;eager 的函数体在调用那一刻(eager(1))就开始执行了,后面的 yield* 步骤才等到运行时。
函数体里有异步 effect 会怎样?
官方 JSDoc 对它的描述是:全部 yield 的 effect 都是同步的,才会完全 eager。一旦函数体里出现异步 effect(比如 Effect.sleep),eager 只会执行到那一步之前,剩下的交给运行时。
我实测过这种情况:异步之前的同步部分先于调用之后的下一行代码,异步之后的部分在运行之后才发生。
什么时候用,什么时候不用?
规则很窄:
- 用在函数体前缀纯计算、无副作用的热路径上,比如参数归一化、缓存命中的快路径。
- 前缀里有日志、写入、读取可变状态这类副作用时,一律用
fnUntraced,因为前缀会在调用点提前发生,和「描述」的预期不符。 - 不确定时用
fnUntraced。性能数字在空函数体上的量级差距,见选择那篇里的微基准,别拿它当对 I/O 型函数的收益预期。
它比 fnUntraced 快多少?
同一个微基准里,fnUntracedEager 约 0.06 微秒,fnUntraced 约 0.4,带 span 的 Effect.fn 约 3.5,返回 Effect.gen 的普通函数约 0.8。这是空函数体、同步、带 runSync 开销的结果,只说明量级关系,绝对数字请在你的环境重测。
版本是 effect 4.0.5,本文的日志输出在这个版本上实际跑过;性能数字是我这台机器上的微基准,只能看量级。「全部 yield 的 effect 都是同步才完全 eager」引自官方 JSDoc 的描述,我没有单独核对原文;异步 effect 出现后 eager 部分的行为我跑过,但这里没有附代码。