为什么要读源码
读 React 源码不是面试要求,也不是为了让简历更好看。读源码的目的是建立对 React 运行机制的直觉,让你在遇到问题时能快速定位原因,在设计组件时能做出正确的判断。
比如,当你理解 useEffect 的清理函数在什么时候被调用后,就不会写出在 useEffect 内部 setState 导致无限循环的 bug。当你理解 React 的批处理机制后,就不会困惑为什么连续三次 setState(count + 1) 最终只让 count 增加 1。
React 源码的结构
React 源码仓库(github.com/facebook/react)是目前世界上最大的前端开源项目之一,完整阅读是不现实的。但理解核心路径是可行的。
React 源码的核心在 packages/react-reconciler 目录下。这个包是 React 的调和器,负责决定如何更新 UI,但不知道具体怎么渲染到某个平台。
packages/react-dom 是 React DOM 的渲染器,负责把 React 的指令应用到浏览器 DOM 上。packages/react-native 是 React Native 的渲染器,原理类似。
这种分离让 React 能同时支持多个渲染目标:浏览器、Native、App Store、甚至是命令行。不同的渲染器只需要实现相同的接口,就能接入 React 的调和器。
一个 setState 的完整旅程
当你在函数组件里调用 setState 时,React 内部发生了以下事情:
第一步:触发更新
setState 内部会调用 dispatchAction,这个函数会创建一个 update 对象,放入组件的 updateQueue 队列里。然后它会检查当前是否在 React 的渲染阶段,如果不在,就会调用 scheduleUpdateOnFiber 来调度一次更新。
第二步:调度优先级
scheduleUpdateOnFiber 会根据 update 的 lane(优先级)来决定什么时候处理这个更新。低优先级的更新可能被延迟,高优先级的更新(如用户点击)会立即处理。
第三步:workLoop
在 workLoop 中,React 会从根节点开始遍历 Fiber 树,寻找需要更新的节点。对于每个需要更新的节点,它会调用 beginWork,这个函数会根据组件的类型(函数组件、类组件、HostComponent 等)调用不同的更新函数。
第四步:Render Phase
对于函数组件,beginWork 会调用 updateFunctionComponent,它会重新执行组件函数。然后 React 会得到新的 React Element 树,与旧的 Fiber 树做对比(diff),生成新的 Fiber 树,并标记有变化的节点(effectTag)。
第五步:Commit Phase
Render Phase 完成后,React 进入 Commit Phase。这个阶段会同步执行所有标记有副作用的节点:插入或删除 DOM 节点、更新 DOM 属性、执行 useEffect 的回调和 useLayoutEffect 的同步回调。
Commit Phase 是同步的,不能被打断,因为它直接操作 DOM,打断会导致 DOM 处于不一致状态。
beginWork 做了什么
beginWork 是 Render Phase 的核心函数。它的基本逻辑是:
function beginWork(current, workInProgress, renderLanes) {
// 根据组件类型分发到不同的更新函数
switch (workInProgress.tag) {
case FunctionComponent:
return updateFunctionComponent(current, workInProgress, renderLanes);
case ClassComponent:
return updateClassComponent(current, workInProgress, renderLanes);
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// ...
}
}
对于函数组件,updateFunctionComponent 会重新执行组件函数:
function updateFunctionComponent(current, workInProgress, renderLanes) {
const Component = workInProgress.type;
const props = workInProgress.pendingProps;
// 重新执行组件函数,得到新的 children
const children = renderWithHooks(
current,
workInProgress,
Component,
props,
renderLanes
);
// reconcile 新的 children
workInProgress.child = reconcileChildFibers(
workInProgress,
workInProgress.child,
nextChildren,
renderLanes
);
return workInProgress.child;
}
renderWithHooks 是执行函数组件的地方。它会设置当前的 dispatch 为当前 fiber 的 dispatch,然后执行组件函数。结果会被用于后续的 diff 过程。
commitRoot 做了什么
当 Render Phase 完成后,React 调用 commitRoot 进入 Commit Phase。
function commitRoot(root) {
// 处理所有 effect
const finishedWork = root.current.finishWork;
// 先处理 beforeMutation 阶段的 effect
// (对应 useLayoutEffect 的同步回调)
commitBeforeMutationEffects();
// 处理 DOM 变更
commitMutationEffects(root, finishedWork);
// 处理 layout effects
// (对应 useLayoutEffect)
commitLayoutEffects(root, finishedWork);
// 处理 passive effects
// (对应 useEffect)
flushPassiveEffects();
return null;
}
commitBeforeMutationEffects 在 DOM 变更之前执行,处理 useLayoutEffect 的同步回调之前的一些准备工作。commitMutationEffects 是实际应用 DOM 变更的地方——插入、更新、删除 DOM 节点。commitLayoutEffects 执行 useLayoutEffect 的销毁函数和回调。flushPassiveEffects 在所有同步工作完成后执行,处理 useEffect。
这就是为什么 useLayoutEffect 的回调是同步执行的(DOM 变更后立即执行),而 useEffect 的回调是异步执行的(在所有同步工作完成后,浏览器空闲时执行)。
Hooks 的实现原理
Hooks 的实现依赖于 Fiber 节点上的 memoizedState 属性。每次组件渲染时,renderWithHooks 会重置当前的 hook 索引,然后依次执行每个 Hook 调用。
function renderWithHooks(current, workInProgress, Component, props, nextRenderLanes) {
workInProgress.memoizedState = null;
currentlyRenderingFiber = workInProgress;
// 重置 hook 索引
hookIndex = 0;
workInProgress.memoizedState = null;
// 执行组件函数,函数内每次调用 useXxx 都会创建或读取 hook
children = Component(props, secondArg);
// 渲染完成后重置
currentlyRenderingFiber = null;
return children;
}
useState 的实现大致是:
function useState(initialState) {
const hook = mountWorkInProgressHook(); // 创建或读取 hook
if (hook.memoizedState === null) {
// 初始化
const setState = dispatchAction.bind(null, currentlyRenderingFiber, queue, initialState);
hook.memoizedState = [initialState, setState];
return [initialState, setState];
}
// 更新
return [hook.memoizedState[0], hook.memoizedState[1]];
}
dispatchAction 就是 setState 背后的函数。它做的事情是:把新的状态放入 updateQueue,然后调度一次更新。
理解源码能帮你做什么
当你理解了 React 内部的调度机制后,很多之前看起来像是"bug"或者"设计缺陷"的行为就变得可以解释了:
setState为什么有时候是异步的——因为 React 的批处理机制,在事件处理函数中多个setState会被合并useEffect为什么在下一次 DOM 更新后才执行——因为useEffect是在flushPassiveEffects里异步执行的,而flushPassiveEffects是在 Commit Phase 完成后才调用的useLayoutEffect为什么是同步的——因为它是在commitLayoutEffects里同步执行的,DOM 变更后立即执行React.memo为什么有时候不起作用——因为每次渲染传给子组件的 props 引用可能都是新的,React.memo只能比较引用,不能比较值
这一章想说的
React 源码是庞大的,但核心路径是清晰的:setState 触发更新 -> 调度优先级 -> workLoop 遍历 Fiber 树 -> Render Phase diff 并标记 effect -> Commit Phase 同步应用 DOM 变更和副作用。
Hooks 的实现依赖于 Fiber 节点的 memoizedState,每个 Hook 在组件渲染时按顺序被创建或读取。
理解源码不是为了记住每一行代码,而是为了建立对 React 运行机制的直觉。这种直觉能帮你在遇到问题时快速定位原因,在设计组件时做出正确的架构决策。