通俗易懂讲 React 原理-第三集:渲染阶段

27 阅读6分钟

宏观的两大阶段

从宏观的角度来说,当页面更新的时候,React 会经历两个重要的阶段:

  • 协调阶段 (Reconciliation Phase):这是一个“计算”阶段。React 在内存中根据新的 ReactElement 树构建或更新 Fiber 树,并标记出所有需要变更的地方(即副作用)。这个阶段是可中断的,所有工作都在内存中完成,不会操作真实的 DOM。
  • 提交阶段 (Commit Phase):这是一个“执行”阶段。React 将协调阶段计算出的所有副作用一次性应用到真实的 DOM 上。这个阶段是不可中断的,必须同步完成,以保证 UI 的一致性。

可以了解几个重要的函数:

协调阶段

beginWork 函数

beginWork 是协调阶段的入口,用于处理一个 Fiber 节点并开始其工作。它的主要任务是向下遍历 Fiber 树。

但并不会让每个节点都经历以下完整步骤的处理,React有一个重要的优化叫做"bailout"(退出)。如果一个组件的props和state都没有变化,React会跳过这个组件及其子树的beginWork过程。这是通过比较当前props和上次渲染的props来实现的

工作流程:

  1. React Scheduler 从根节点开始(没错每一次更新都要从根节点开始),调用 beginWork(rootFiber)。
  2. beginWork 根据 fiber.tag 路由到不同的处理函数(如 updateFunctionComponent)。
  3. 处理函数执行组件逻辑,得到子元素的 ReactElement(子元素指的就是组件 return 的内容)。
  4. reconcileChildren 函数被调用,把子元素的 ReactElement 与当前 Fiber 节点的子节点进行 Diff 算法,生成或更新子 Fiber 树,并设置好子节点的 return, sibling 等指针。
  5. beginWork 返回第一个子节点,渲染循环继续对子节点调用 beginWork,如此递归下去,直到遍历到最深的叶子节点。

completeWork 函数

当 beginWork 返回 null(到达叶子节点)后,遍历过程会“回溯”。completeWork 就是在这个回溯过程中被调用的,用于完成一个 Fiber 节点的工作。它的主要任务是向上收集信息。

工作流程:

  1. 当一个节点的所有子节点都 completeWork 完成后,轮到它自己 completeWork。
  2. 对于 div 这样的节点,它会创建 div 这个 DOM 元素,并把所有子节点(比如

    的 DOM)都 append 进去。并将其赋值给 fiber.stateNode。

  3. 处理 props:将 pendingProps 中的变化应用到 stateNode (DOM 节点) 上,例如更新 className, style 等。
  4. 冒泡副作用:检查所有子节点的 flags(副作用标记),并将它们聚合(通过位运算 |)到当前节点的 subtreeFlags 属性上。这样,父节点就能快速知道它的子树中是否存在副作用,而无需遍历所有子节点。
  5. 完成后,它告诉渲染循环:“我的工作做完了,请去处理我的兄弟节点吧。” 如果没有兄弟,就说:“请去处理我的父节点吧。”

提交阶段

commitMutationEffects 函数

这是提交阶段的第一个子阶段。它的核心任务是执行所有会改变真实 DOM 的副作用。将协调阶段计算出的所有 DOM 变更(增、删、改)应用到屏幕上。这个阶段是同步且不可中断的,以确保用户看到的 UI 始终是完整的、一致的。

工作流程:

  1. 协调阶段完成后,React 得到了一棵带有完整 flags 和 subtreeFlags 信息的 workInProgress Fiber 树。
  2. React 进入提交阶段,首先调用 commitMutationEffects。
  3. 它从根节点开始,根据 subtreeFlags 快速定位到有副作用的子树。
  4. 它按照特定顺序(例如,先处理所有 Deletion,再处理 Placement,最后处理 Update)执行 DOM 操作。
  5. 在这个阶段结束后,屏幕上的 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 都经历了哪些变化?