React 的渲染机制
1. React 在哪些情况下会执行函数组件?
先把一个概念讲清楚:函数组件的执行 = 组件的「渲染(render)阶段」。
在 React 中,每一次执行函数组件,都是在「计算这一次应该长成什么样(返回什么 JSX)」——并不是每次执行都会真实更新 DOM,真实 DOM 更新只在 commit 阶段发生。
React 会在以下几种典型场景重新执行函数组件:
1)父组件重新渲染
父组件函数执行 → 父组件返回的 JSX 中包含子组件 → 子组件也会重新执行函数组件。
默认情况:只要父组件重新 render,子组件就会跟着重新 render(不管 props 有没有变)。2)当前组件自身的 state 变化
调用setState/setXxx(useState)会把组件标记为需要更新,在下一轮调度中重新执行该组件函数。3)当前组件接收到新的 props
父组件传入的 props 引用发生变化(或值发生变化)时,当前组件在下一轮更新中会重新执行.4)触发了 context 变更(useContext)
当某个Context.Provider的value发生变化时,所有消费这个 context 的组件(useContext或Context.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:值级别的计算缓存
作用: 缓存一个“计算结果”,只在依赖项变化时重新执行计算函数.
const memoizedValue = useMemo(() => {
// 一些比较耗时或不希望每次都重新计算的逻辑
return computeSomething(props.a, props.b);
}, [props.a, props.b]);适用场景:
- 复杂计算:如大列表过滤、排序、聚合。
- 需要稳定引用的派生数据:比如将一个对象作为 props 传给子组件,希望在依赖不变时保持同一个引用.
注意点:
useMemo本身也有开销,不要为所有小计算都上useMemo,要关注“是否热路径 + 是否真正昂贵”。- 依赖数组写错(少写 / 多写)会导致:要么缓存不生效,要么逻辑错误.
简单说:
useMemo是“记住某个值的计算结果,并控制它什么时候重新算”。
2.3 useCallback:函数级别的引用稳定
作用: 返回一个“引用稳定”的函数,只在依赖项变化时生成新的函数引用.
const handleClick = useCallback(() => {
doSomething(count);
}, [count]);适用场景:
- 把函数作为 props 传给子组件,结合
memo或shouldComponentUpdate. - 把函数作为依赖传入
useEffect/ 自定义 hooks,希望避免引用每次都变. - 事件处理函数很多,但实际变动不频繁.
- 把函数作为 props 传给子组件,结合
useCallback(fn, deps)等价于useMemo(() => fn, deps),只是语义更清晰:它其实就是给“函数”用的useMemo.滥用风险:
- 为了“强迫所有函数都用
useCallback”,结果依赖数组特别复杂,开发成本变高. - 如果子组件没有
memo,你稳定了函数引用也不会减少渲染.
- 为了“强迫所有函数都用
一句话:
useCallback主要是为“下游优化”服务的 —— 保证传给别人的函数引用是稳定可比较的.
2.4 一个典型优化组合示例
整体思路通常是:
- 组件层:对“重型子组件”使用
memo. - 值层:用
useMemo计算复杂 / 派生数据,顺带保持引用稳定. - 函数层:用
useCallback保持事件处理函数引用稳定,配合memo子组件.
常见套路:
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,只要list和onSelect引用没变,就会跳过重新执行.- 从而切断了父组件频繁更新对
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 不会遇到性能问题,而是设计取舍不同:
更新粒度不同
Vue 的响应式 + 编译优化,让它的每次更新往往只涉及比较小的一部分组件树.
同等场景下,单次更新工作量更小,不太容易触发“长时间阻塞主线程”的极端情况.优先选择工程层面的解决方案
Vue 更鼓励在架构和工程层面解决“大型页面性能”问题:分页 / 虚拟列表、路由级拆分、懒加载 / keep-alive 等.
对于绝大多数业务场景,这些手段已经足够.实现成本与复杂度权衡
Fiber 带来的是整个渲染内核极大的复杂度提升。React 选择了“为未来更激进的并发能力买单”;Vue 则暂时倾向于用更简单、可控的渲染模型,配合响应式系统做细粒度更新.目标与定位略有不同
React 非常关注“可插拔的调度层”和“未来可能的渲染目标”(不仅仅是 DOM,还有 Native、VR 等);Vue 目前主要聚焦在 Web / 小程序等相对收敛的目标上,现有模型足够支撑.
所以可以归纳为一句:
React 需要 Fiber,是因为它的渲染模型天然是“自上而下、可能很重”的,而 Fiber 让这件事变得可中断、可调度;Vue 靠响应式系统把更新粒度压得更细,在大部分场景下不必为所有场景引入同等复杂度的机制.