Skip to main content

什么是 once()

once() 是一个执行回调,并在整个请求期间把结果缓存在内存中的全局辅助函数。当从同一调用位置以相同回调再次调用时,会返回已缓存的结果。
从对象实例的方法内部调用时,缓存以该实例为单位相互独立。
这种「每个调用位置、每个实例」独立缓存的机制,与 memoize 这类简单的记忆化辅助函数不同。要理解其原理,需要看 Illuminate\Support\OnceIlluminate\Support\Onceable 两个类。

helpers.php 中的调用

Illuminate\Support\helpers.phponce() 函数本体非常简单。
关键点在于:once() 本身并不持有缓存,而是根据 debug_backtrace() 获取的调用方信息组装出 Onceable 实例,然后把处理委托给 Once 类。

Onceable — 用于唯一定位调用位置的哈希计算

Onceable 类负责根据调用栈计算出用于唯一定位「哪个调用位置」的哈希。
哈希的输入信息是 文件路径 + 类名 + 函数名 + 行号 + 闭包 use 引用变量的值。也就是说,即便从同一行调用 once(),只要 use 引用的变量值发生变化,就会被视为不同的缓存
eval() 出来的代码中调用 once() 时,hashFromTrace() 会返回 null,完全不进行缓存。这是为了对 Blade 编译后的视图等经由 eval 执行的场景做安全处理。
object 表示「调用方是否是实例方法」。$trace[1]['object']debug_backtrace() 上一层(调用 once() 的一方)的对象,所以在实例方法内就是 $this,在静态方法或全局函数中则为 null

Once — 基于 WeakMap 的缓存本体

真正持有缓存的是 Illuminate\Support\Once
关键技术是 WeakMap。以 $onceable->object(调用方实例)作为键,值是「哈希 → 结果」的关联数组。当没有 object(全局函数或静态方法)时,键使用 Once 自身的 $this,实际上就形成了在整个进程内共享的单一缓存区域。
由于使用了 WeakMap,一旦作为键的对象在其他地方失去引用,它所关联的缓存也会成为垃圾回收对象。即便从大量对象调用 once(),也不容易造成内存泄漏。

在测试中活用 — enable / disable / flush

调用 Once::disable() 会完全禁用缓存,once() 每次都会执行回调。测试中「每次都想得到新值」时非常有用。
Once::flush() 会清空整个缓存,并在下次访问时新建 WeakMap。用于 Artisan 命令测试,或在 Octane 那样进程会被复用的环境中,避免缓存跨请求残留的场景。

PreventsCircularRecursion — Onceable 的应用示例

Onceable::tryFromTrace() 并非只服务于 once(),Eloquent 的 Illuminate\Database\Eloquent\Concerns\PreventsCircularRecursion trait 也复用了它。该 trait 用于「防止同一对象的同一调用位置在调用栈中重入」。
Once 的决定性区别在于:在 finally 块中调用完成后立即清空缓存Once 的定位是「请求期间持续缓存」,而 PreventsCircularRecursion 则复用相同的 Onceable 机制,实现「仅在当前调用栈期间缓存(实际上是加锁)」的效果。 Eloquent 模型的典型用法,是防止模型的 toArray() 或访问器递归引用自身。

在包开发中的应用

在自定义包中想实现「同一实例的同一方法调用在一次请求中只执行一次」时,直接使用 once() 是最简单的方式。虽然直接使用 Onceable/Once 的场景不多,但在下列场景中理解其内部实现会很有帮助。
  • 在 Artisan 命令或测试中,想通过 Once::disable() / Once::flush() 控制缓存行为时
  • 在自定义 trait 中想实现「以调用位置为单位」的缓存或防重入时(可以复用 PreventsCircularRecursion 的相同模式)
  • once() 的结果与预期不同、需要排查时,理解哈希输入要素(文件、类、函数、行号、use 变量)能更快定位原因
once() 会把闭包 use 引用的变量也纳入哈希,因此在循环中传入每次都捕获不同值的闭包时,可能会意外地在每次迭代得到不同的缓存键,实际上完全没起到缓存效果。在循环中使用时,请确认缓存粒度是否符合预期。

相关页面

tap() 辅助函数与 Tappable trait

在保持返回值不变的同时插入副作用的 tap() 辅助函数的实现模式。

Eloquent Observer 与模型事件

通过 Eloquent 模型的事件与 Observer 进行集中管理。
最后修改于 2026年9月11日