Skip to content

React 的渲染机制

1. React 在哪些情况下会执行函数组件?

先把一个概念讲清楚:函数组件的执行 = 组件的「渲染(render)阶段」

在 React 中,每一次执行函数组件,都是在「计算这一次应该长成什么样(返回什么 JSX)」——并不是每次执行都会真实更新 DOM,真实 DOM 更新只在 commit 阶段发生。

React 会在以下几种典型场景重新执行函数组件:

  • 1)父组件重新渲染
    父组件函数执行 → 父组件返回的 JSX 中包含子组件 → 子组件也会重新执行函数组件。
    默认情况:只要父组件重新 render,子组件就会跟着重新 render(不管 props 有没有变)。

  • 2)当前组件自身的 state 变化
    调用 setState / setXxxuseState)会把组件标记为需要更新,在下一轮调度中重新执行该组件函数。

  • 3)当前组件接收到新的 props
    父组件传入的 props 引用发生变化(或值发生变化)时,当前组件在下一轮更新中会重新执行.

  • 4)触发了 context 变更(useContext)
    当某个 Context.Providervalue 发生变化时,所有消费这个 context 的组件(useContextContext.Consumer)会重新执行.

  • 5)触发了强制更新(类组件中的 forceUpdate 或某些内部机制)
    在函数组件里通常不直接使用,但一些库内部可能会使用类似机制导致组件重新执行.

  • 6)严格模式(StrictMode)下的额外渲染(开发环境)
    React 18 开发环境下,StrictMode 会刻意双调用某些生命周期 / 函数组件,用于帮助你发现副作用问题。这只发生在开发环境,生产环境不会双渲染.

关键结论: 在默认情况下,只要父组件重新渲染,子组件就会重新渲染。这是 React 的「自上而下 diff」模型决定的,也是我们为什么需要各种“剪枝型优化”的根源.


2. 如何对函数组件渲染做优化?

优化的核心目标只有一个:在不改变 UI 结果的前提下,尽可能减少「不必要的函数执行次数」和「不必要的计算」.

典型手段主要有三类:

  • 组件级别:React.memo
  • 值级别:useMemo
  • 函数级别:useCallback

它们解决的问题不同,但都围绕一个关键词:“引用稳定性”.

2.1 React.memo:组件级剪枝

作用: 记忆组件的“上一次 props”,当下一次渲染时,如果 props(浅比较)没有变化,就跳过函数组件的执行.

  • 适用场景:

    • 子组件依赖的 props 相对简单(基本类型或稳定的引用)。
    • 父组件会频繁渲染,但子组件的 props 大部分时间不变。
    • 列表中的 row 项、复杂表单中某个子区域等.
  • 工作原理(默认)memo 内部会对上一轮 props 和这一轮 props 做一次浅比较(shallowEqual):

    • 基本类型直接比较值。
    • 引用类型比较引用是否相等(同一个对象 / 数组 / 函数引用)。

如果“相等”,跳过渲染;否则执行组件函数.

  • 常见问题: 父组件每次渲染都重新创建了 [] / {} / () => {},导致 props 引用每次都变 → memo 失效.
    这就是为什么我们经常需要配合使用 useMemo / useCallback 来保证 props 引用稳定.

可以理解为:memo 是“门口保安”,但是否能认出“老熟人”,取决于你是不是每次都换了衣服(新的引用)。


2.2 useMemo:值级别的计算缓存

作用: 缓存一个“计算结果”,只在依赖项变化时重新执行计算函数.

ts
const memoizedValue = useMemo(() => {
  // 一些比较耗时或不希望每次都重新计算的逻辑
  return computeSomething(props.a, props.b);
}, [props.a, props.b]);
  • 适用场景:

    • 复杂计算:如大列表过滤、排序、聚合。
    • 需要稳定引用的派生数据:比如将一个对象作为 props 传给子组件,希望在依赖不变时保持同一个引用.
  • 注意点:

    • useMemo 本身也有开销,不要为所有小计算都上 useMemo,要关注“是否热路径 + 是否真正昂贵”。
    • 依赖数组写错(少写 / 多写)会导致:要么缓存不生效,要么逻辑错误.

简单说:useMemo 是“记住某个值的计算结果,并控制它什么时候重新算”。


2.3 useCallback:函数级别的引用稳定

作用: 返回一个“引用稳定”的函数,只在依赖项变化时生成新的函数引用.

ts
const handleClick = useCallback(() => {
  doSomething(count);
}, [count]);
  • 适用场景:

    • 把函数作为 props 传给子组件,结合 memoshouldComponentUpdate.
    • 把函数作为依赖传入 useEffect / 自定义 hooks,希望避免引用每次都变.
    • 事件处理函数很多,但实际变动不频繁.
  • useCallback(fn, deps) 等价于 useMemo(() => fn, deps),只是语义更清晰:它其实就是给“函数”用的 useMemo.

  • 滥用风险:

    • 为了“强迫所有函数都用 useCallback”,结果依赖数组特别复杂,开发成本变高.
    • 如果子组件没有 memo,你稳定了函数引用也不会减少渲染.

一句话:useCallback 主要是为“下游优化”服务的 —— 保证传给别人的函数引用是稳定可比较的.


2.4 一个典型优化组合示例

整体思路通常是:

  • 组件层:对“重型子组件”使用 memo.
  • 值层:用 useMemo 计算复杂 / 派生数据,顺带保持引用稳定.
  • 函数层:用 useCallback 保持事件处理函数引用稳定,配合 memo 子组件.

常见套路:

tsx
const Child = React.memo(function Child({ list, onSelect }) {
  // ...
});

function Parent({ data }) {
  const filtered = useMemo(() => data.filter((item) => item.visible), [data]);

  const handleSelect = useCallback((id: string) => {
    console.log("select", id);
  }, []);

  return <Child list={filtered} onSelect={handleSelect} />;
}

在这个例子中:

  • Parent 每次渲染不一定会改变 filtered / handleSelect 的引用.
  • Child 使用了 memo,只要 listonSelect 引用没变,就会跳过重新执行.
  • 从而切断了父组件频繁更新对 Child 的“传染式渲染”.

3. React vs Vue:为什么 React 需要 Fiber,而 Vue 不需要?

这一节重点对比两者的渲染模型,从而理解 Fiber 存在的必要性.

3.1 渲染模型与响应式思路的差异

Vue:以响应式为中心的「按需更新」模型

  • Vue(无论 2 还是 3)都围绕响应式系统展开:
    • 组件渲染函数会收集依赖(响应式数据)。
    • 当某个响应式数据变化时,只会触发依赖它的那部分组件树重新渲染.
    • 模板编译阶段还能做静态提升、静态 patch flag 等优化,只更新必要的 VNode.

直观理解:

  • Vue 知道“谁依赖了谁”,所以更新可以比较“定向”.
  • 默认就避免了很多不必要的向下传播渲染.

React:以状态和 props 传递为中心的自上而下 diff 模型

  • React 不做响应式依赖收集:
    • 组件内部使用 useState / useReducer / useContext 等管理状态.
    • 一旦某个组件被标记为需要更新,就会触发从该组件为根的子树 diff:所有子组件都重新执行函数组件(除非使用 memo 等手动剪枝)。

直观理解:

  • React 不知道“某个 state 只影响了模板中的哪一小块”,它只知道“这个组件更新了,需要重新计算整棵子树”.
  • 因此天然会有更多“潜在的、可以被优化掉的重新执行”.

因此:Vue 把“按需更新”的能力内置在框架的响应式系统里;React 则更偏向“暴露给用户,用 memo / useMemo 等自己剪枝”.


3.2 Fiber 的本质:可中断 / 可调度的渲染过程

Fiber 的核心价值并不只是“提高性能”,而是:

让渲染变为可中断、可恢复、可分片调度的过程.

对比一下老的 Stack Reconciler(React 15 及以前):

  • 更新从根组件开始,递归遍历整棵组件树:
    • 过程是同步、不可中断的.
    • 如果组件树很大、更新很多,就可能长时间霸占主线程,导致用户输入卡顿、动画掉帧、页面“假死”几百毫秒.

Fiber 做了什么?

  • 把“递归遍历”拆成“可中断的循环 + Fiber 节点链表结构”:

    • 渲染阶段(render phase)可以被拆成很多小任务,每处理完一小块 Fiber 节点,就检查当前时间片是否用完.
    • 如果时间片用完 → 暂停这次渲染,把控制权还给浏览器(让浏览器处理用户输入、动画等)。
    • 下一次有空闲时间时,再从中断处继续构建 Fiber 树.
    • 直到整棵更新树构建完成,再一次性进入 commit 阶段更新 DOM.
  • 同时配合优先级调度(Lane 模型):

    • 用户输入、动画等高优任务可以打断低优任务(比如列表渲染)。

这就是为什么 React 后来可以实现「并发特性」:背后全靠 Fiber 的可中断 + 优先级调度能力.


3.3 为什么 Vue 暂时“不需要” Fiber?

这里的“不需要”并不是说 Vue 不会遇到性能问题,而是设计取舍不同

  1. 更新粒度不同
    Vue 的响应式 + 编译优化,让它的每次更新往往只涉及比较小的一部分组件树.
    同等场景下,单次更新工作量更小,不太容易触发“长时间阻塞主线程”的极端情况.

  2. 优先选择工程层面的解决方案
    Vue 更鼓励在架构和工程层面解决“大型页面性能”问题:分页 / 虚拟列表、路由级拆分、懒加载 / keep-alive 等.
    对于绝大多数业务场景,这些手段已经足够.

  3. 实现成本与复杂度权衡
    Fiber 带来的是整个渲染内核极大的复杂度提升。React 选择了“为未来更激进的并发能力买单”;Vue 则暂时倾向于用更简单、可控的渲染模型,配合响应式系统做细粒度更新.

  4. 目标与定位略有不同
    React 非常关注“可插拔的调度层”和“未来可能的渲染目标”(不仅仅是 DOM,还有 Native、VR 等);Vue 目前主要聚焦在 Web / 小程序等相对收敛的目标上,现有模型足够支撑.

所以可以归纳为一句:

React 需要 Fiber,是因为它的渲染模型天然是“自上而下、可能很重”的,而 Fiber 让这件事变得可中断、可调度;Vue 靠响应式系统把更新粒度压得更细,在大部分场景下不必为所有场景引入同等复杂度的机制.


本站所有内容均为原创,转载请注明出处