宏观的两大阶段
从宏观的角度来说,当页面更新的时候,React 会经历两个重要的阶段:
- 协调阶段 (Reconciliation Phase):这是一个“计算”阶段。React 在内存中根据新的 ReactElement 树构建或更新 Fiber 树,并标记出所有需要变更的地方(即副作用)。这个阶段是可中断的,所有工作都在内存中完成,不会操作真实的 DOM。
- 提交阶段 (Commit Phase):这是一个“执行”阶段。React 将协调阶段计算出的所有副作用一次性应用到真实的 DOM 上。这个阶段是不可中断的,必须同步完成,以保证 UI 的一致性。
可以了解几个重要的函数:
协调阶段
beginWork 函数
beginWork 是协调阶段的入口,用于处理一个 Fiber 节点并开始其工作。它的主要任务是向下遍历 Fiber 树。
但并不会让每个节点都经历以下完整步骤的处理,React有一个重要的优化叫做"bailout"(退出)。如果一个组件的props和state都没有变化,React会跳过这个组件及其子树的beginWork过程。这是通过比较当前props和上次渲染的props来实现的
工作流程:
- React Scheduler 从根节点开始(没错每一次更新都要从根节点开始),调用 beginWork(rootFiber)。
- beginWork 根据 fiber.tag 路由到不同的处理函数(如 updateFunctionComponent)。
- 处理函数执行组件逻辑,得到子元素的 ReactElement(子元素指的就是组件 return 的内容)。
- reconcileChildren 函数被调用,把子元素的 ReactElement 与当前 Fiber 节点的子节点进行 Diff 算法,生成或更新子 Fiber 树,并设置好子节点的 return, sibling 等指针。
- beginWork 返回第一个子节点,渲染循环继续对子节点调用 beginWork,如此递归下去,直到遍历到最深的叶子节点。
completeWork 函数
当 beginWork 返回 null(到达叶子节点)后,遍历过程会“回溯”。completeWork 就是在这个回溯过程中被调用的,用于完成一个 Fiber 节点的工作。它的主要任务是向上收集信息。
工作流程:
- 当一个节点的所有子节点都 completeWork 完成后,轮到它自己 completeWork。
- 对于 div 这样的节点,它会创建 div 这个 DOM 元素,并把所有子节点(比如
和 的 DOM)都 append 进去。并将其赋值给 fiber.stateNode。
- 处理 props:将 pendingProps 中的变化应用到 stateNode (DOM 节点) 上,例如更新 className, style 等。
- 冒泡副作用:检查所有子节点的 flags(副作用标记),并将它们聚合(通过位运算 |)到当前节点的 subtreeFlags 属性上。这样,父节点就能快速知道它的子树中是否存在副作用,而无需遍历所有子节点。
- 完成后,它告诉渲染循环:“我的工作做完了,请去处理我的兄弟节点吧。” 如果没有兄弟,就说:“请去处理我的父节点吧。”
提交阶段
commitMutationEffects 函数
这是提交阶段的第一个子阶段。它的核心任务是执行所有会改变真实 DOM 的副作用。将协调阶段计算出的所有 DOM 变更(增、删、改)应用到屏幕上。这个阶段是同步且不可中断的,以确保用户看到的 UI 始终是完整的、一致的。
工作流程:
- 协调阶段完成后,React 得到了一棵带有完整 flags 和 subtreeFlags 信息的 workInProgress Fiber 树。
- React 进入提交阶段,首先调用 commitMutationEffects。
- 它从根节点开始,根据 subtreeFlags 快速定位到有副作用的子树。
- 它按照特定顺序(例如,先处理所有 Deletion,再处理 Placement,最后处理 Update)执行 DOM 操作。
- 在这个阶段结束后,屏幕上的 DOM 已经更新,但一些生命周期(如 componentDidMount,useEffect)和 ref 的回调还没有执行。
commitLayoutEffects 函数
commitLayoutEffects 会执行 useEffect 的回调、componentDidMount/Update 等生命周期,并更新 ref,此时 DOM 已经更新完毕,可以安全地读取 DOM 信息了。
useEffect 的回调函数是在 commitLayoutEffects 阶段被调度,但实际执行是在微任务队列中异步进行的,这确保了它不会阻塞浏览器绘制。
useLayoutEffect 的回调函数则是在 commitLayoutEffects 阶段同步执行的,这发生在 DOM 更新后但浏览器绘制前。
在 React 18 中,commit 阶段不再使用 effectList 链表,而是改为深度优先遍历整个 Fiber 树,使用 subtreeFlags 进行优化。
lanes 模型的继承
在 beginWork 开始时,React 会检查当前 Fiber 节点的 lanes 是否与本次渲染的优先级匹配。如果不匹配(比如这是一个低优先级的更新,而当前正在处理高优先级渲染),React 会直接跳过这个 Fiber 及其整个子树的 beginWork 和 completeWork。
这使得 React 可以只处理与当前优先级相关的部分 Fiber 树,极大地提升了效率。
Suspense 与 Transitions 原理
- Suspense:在 beginWork 阶段,如果组件抛出 Promise(数据未就绪),React 会捕获它,并暂停该 Fiber 子树的渲染。然后它会向上寻找最近的
<Suspense>边界,并渲染其 fallback。这个“暂停”就是利用了协调阶段可中断的特性。 - Transitions:通过 startTransition 包裹的更新会被标记为低优先级。这意味着它们的 beginWork 和 completeWork 过程非常容易被高优先级的用户交互更新所中断。
diff 算法过程
这个过程不是比较两棵 ReactElement 树(因为旧的 ReactElement 树已经不存在了),也不是比较两棵 Fiber 树(因为新的 Fiber 树正在构建过程中),而是用新的 ReactElement 作为"蓝图"来更新现有的 Fiber 树。
React 的 协调阶段,就是拿着新的 ReactElement 树(蓝图),去对比 current Fiber 树(现有建筑),然后在内存中一棵节点一棵节点地构建 workInProgress Fiber 树(新建筑蓝图)。
- 如果 current 树中的某个 Fiber 节点和“蓝图”中的 ReactElement 匹配(类型、key 都相同),React 就会复用这个 Fiber 节点,只更新必要的属性(如 props),然后将它“克隆”到 workInProgress 树中。
- 如果不匹配,React 就会标记这个 Fiber 节点需要被删除,并在 workInProgress 树中创建一个全新的 Fiber 节点。
当 workInProgress 树构建完成后,React 会进入 提交阶段,用这棵“新建筑”去高效地替换屏幕上的“旧建筑”(current 树),然后让 current 指针指向这棵新树。
总结:React 的 Diff 算法,是在协调阶段,以新的 ReactElement 树为蓝图,遵循“层级、类型、key”三大原则,对现有的 current Fiber 树进行线性遍历和更新,从而在内存中高效地构建出 workInProgress Fiber 树的过程。
尾巴
上面讲的 beginWork, completeWork, commitMutationEffects, commitLayoutEffects 都比较粗糙,实际上他们对 Fiber 上的很多属性做了处理,这些处理几乎都是可中断并且恢复执行的。例如:
- memoizedState 都经历了哪些变化?