为啥hooks要用全局变量(而不是参数传递)
这个问题已经不是“Hooks 怎么实现”,而是 React 为什么只能这样设计。 我们从 “如果不用全局变量会怎样” 反推,你会发现这是一个几乎被逼出来的选择。
先给结论: Hooks 使用全局变量,不是偷懒,而是为了保证: render 可重跑、可中断、零侵入、零编译成本。
# 一、Hooks 到底用到了哪些“全局变量”?
核心只有这几个:
let currentlyRenderingFiber = null
let workInProgressHook = null
let currentHook = null
1
2
3
2
3
再加一个:
ReactCurrentDispatcher.current
1
👉 所有 Hook 都通过这些“隐式上下文”工作
# 二、为什么不能用参数传递?(你一定会想到)
理想中的 API 可能是:
function useState(ctx, initial) { ... }
function App(props, ctx) {
const [a, setA] = useState(ctx, 0)
}
1
2
3
4
5
2
3
4
5
或者更“隐式”一点:
function App(props, ctx = useHooksContext()) { ... }
1
# 看起来是不是很干净?
# 三、参数传递的第一个致命问题:API 被污染
React 的核心目标之一是:
组件函数 = 普通 JS 函数
Component(props) → JSX
1
如果引入参数:
- 每个组件都要接收
ctx - 每个 Hook 都要显式传
- 用户心智复杂度暴涨
👉 这直接违背 React 的设计哲学
# 四、第二个致命问题:render 无法“透明重跑”
Fiber 的核心前提是:
render 是可丢弃、可重试的纯函数
如果 Hook 上下文靠参数:
render(Component, ctx)
1
你必须保证:
- ctx 在多次 render 间一致
- ctx 生命周期与 Fiber 同步
- ctx 在中断 / 恢复时可追溯
👉 你实际上是在“手写 Fiber”
# 五、第三个致命问题:无法做到“零编译”
如果不用全局变量,剩下的方案只有:
# 1️⃣ 编译期注入
useState()
↓ 编译
useState(__hookCtx, ...)
1
2
3
2
3
这意味着:
- 必须有编译器
- 必须锁定 DSL
- 破坏 JS 直写
👉 React 明确拒绝这条路
# 六、全局变量的“隐藏优势”:中断友好
你前面已经理解 Fiber 的执行模型:
renderWithHooks
set currentlyRenderingFiber
execute Component()
clear globals
1
2
3
4
2
3
4
# 中断时会发生什么?
- render 被丢弃
- 全局变量直接 reset
- 不会留下半成品状态
如果是参数:
- 你要保存 ctx
- 要回滚
- 要重放
👉 复杂度指数级上升
# 七、为什么“全局”却是安全的?
这是一个很容易被误解的点。
# 并不是“真正的全局状态”
而是:
render 期间的“动态作用域”
renderWithHooks() {
set globals
Component()
clear globals
}
1
2
3
4
5
2
3
4
5
- 单线程
- 同一时间只 render 一个 Fiber
- 没有并发写冲突
👉 逻辑上是安全的
# 八、Vue effect 对照(帮你对齐)
| Vue3 | React |
|---|---|
| activeEffect | currentlyRenderingFiber |
| effect(fn) | renderWithHooks |
| track 隐式 | Hooks 隐式 |
| 编译增强 | 运行时约定 |
👉 两者都是 “隐式上下文”,只是位置不同。
# 九、如果强行不用全局变量,会怎样?
你会得到一个框架:
- 组件签名被污染
- Hook API 复杂
- render 不再是纯函数
- 中断 / 并发成本爆炸
- 必须引入编译器
👉 这已经不是 React 了
# 十、终极一句话总结(这条是设计哲学)
Hooks 用全局变量,是为了用“最小运行时成本”, 换取“最大调度自由度”和“最干净的用户 API”。
这不是实现细节,是 核心设计选择。
上次更新: 2026/01/08, 10:00:37