我不知道的 React:生命周期演进与 Render 的触发机制
423 字
10 min read
React 生命周期 render Hooks useEffect Fiber 性能优化
类组件生命周期的演变:为何告别旧时代?
在 React Hooks 出现之前,类组件的生命周期方法是管理状态、副作用和性能优化的核心。然而,随着 React 引入 Fiber 架构和异步渲染,一些旧的生命周期方法因其设计缺陷而被标记为不安全 (UNSAFE_
) 并最终被废弃。
被废弃的关键钩子
主要涉及以下三个在 Fiber 架构下存在问题的生命周期方法:
UNSAFE_componentWillMount
: 在组件挂载到 DOM 之前执行。UNSAFE_componentWillUpdate
: 在组件因状态或 Props 变化而即将重新渲染之前执行。UNSAFE_componentWillReceiveProps
: 在已挂载组件接收到新的 Props 之前执行。
废弃的核心原因:与 Fiber 异步渲染的冲突
这些生命周期方法的设计基于 React 早期的同步渲染模型。在 Fiber 架构下,渲染过程(Render Phase)是异步且可中断的,这导致了以下问题:
- 可能被多次调用或不被调用: 由于 Render 阶段可能暂停、中止或重新开始,这些
Will*
方法可能在一个更新周期中被多次触发,或者在最终提交(Commit Phase)前被放弃而不触发对应的Did*
方法。如果在这些方法中执行副作用(如发起网络请求),可能导致状态不一致或资源浪费。 - 副作用的时机问题:
componentWillMount
在 DOM 渲染前执行,此时进行 DOM 操作或需要 DOM 信息的副作用是不安全的。componentWillUpdate
和componentWillReceiveProps
也存在类似的时机问题,它们在 Render 阶段执行,但副作用(如基于新 Props 请求数据)通常应该在 DOM 更新后的 Commit 阶段执行。 - 诱导不良实践: 这些方法常常被误用于派生状态 (
derived state
),导致状态来源混乱和难以维护。React 推荐使用更明确的模式来处理派生状态。
现代替代方案:拥抱 Hooks 与新生命周期
React 提供了更安全、更适应 Fiber 架构的替代方案:
useEffect
(函数组件核心): 用于处理副作用。它在Commit 阶段之后异步执行,保证了执行时 DOM 已经更新,并且其清理机制能有效防止内存泄漏。它不是简单地替代旧生命周期,而是提供了一种基于状态同步和副作用声明的新模型。static getDerivedStateFromProps(props, state)
: 用于替代componentWillReceiveProps
中常见的"根据 Props 更新 State"的场景。它是一个纯函数,在 Render 阶段执行,返回一个对象来更新 state,或者返回null
表示无需更新。getSnapshotBeforeUpdate(prevProps, prevState)
: 在 Commit 阶段之前(DOM 更新前)被调用。它使得组件能在 DOM 可能发生变化之前从中捕捉一些信息(例如滚动位置)。此生命周期方法的任何返回值将作为参数传递给componentDidUpdate()
。componentDidMount()
和componentDidUpdate()
: 这两个方法在 Commit 阶段之后执行,是执行需要访问 DOM 的副作用(如手动 DOM 操作、网络请求)的推荐位置。
这些新的模式和 Hooks 更符合 Fiber 的异步、可中断特性,使得副作用管理更安全、逻辑更清晰。
Render 方法的运行揭秘
render()
方法(或函数组件本身)是 React 组件的核心,它负责根据当前的 Props 和 State 返回要渲染的 UI 描述(虚拟 DOM)。
render
的本质:生成 UI 描述
- 无论是类组件的
render()
方法还是函数组件的返回值,它们的核心任务都是声明性地描述 UI 结构。 - 它们返回的内容可以是 JSX、React 元素、数组、Fragment、字符串、数字或
null
/boolean
(表示不渲染任何东西)。 - 关键点:
render
方法应该是纯函数,意味着对于相同的 Props 和 State,它应该总是返回相同的 UI 描述,并且不应包含任何副作用(如修改全局变量、发起网络请求)。
触发 render
的时机
一个组件的 render
方法(或函数组件本身)会在以下情况被调用(触发重渲染 Re-render):
- 首次挂载 (Initial Mount): 组件第一次被创建并添加到 DOM 时。
- 状态变更: 调用
this.setState()
(类组件) 或状态更新函数 (如useState
返回的setCount
) 时。 - Props 更新: 当父组件传递给它的 Props 发生变化时(浅比较不同)。
- 父组件重渲染: 默认情况下,只要父组件重渲染,其所有子组件(即使 Props 和 State 没有变化)也会触发重渲染。这是 React 默认行为,也是性能优化的重点关注区域。
- 强制更新: 调用
this.forceUpdate()
(类组件,不推荐) 或自定义 HookuseUpdate
时。
render
与 Diff 算法的关系
render
方法的输出是 React Diff 算法(调和过程)的输入。当组件重渲染时:
- React 调用
render
方法得到新的虚拟 DOM 树。 - React 将这个新的虚拟 DOM 树与上一次渲染生成的旧虚拟 DOM 树进行比较 (Diffing)。
- Diff 算法找出两棵树之间的最小差异。
- React DOM (或其他渲染器) 根据这些差异去更新真实的 DOM。
因此,render
方法本身的执行成本(计算逻辑、生成 VDOM 对象)以及它触发的 Diff 和 DOM 更新成本,共同构成了组件更新的性能开销。
优化 render
的策略
避免不必要的 render
调用和减少 render
内部的计算量是 React 性能优化的关键。
避免不必要的重渲染
React.memo
(函数组件): 包裹函数组件,当其 Props 没有发生(浅比较)变化时,阻止该组件重渲染。const MyComponent = React.memo(function MyComponent(props) { /* 仅在 props 变化时渲染 */ });
PureComponent
(类组件): 继承React.PureComponent
而不是React.Component
。它会自动实现一个基于 Props 和 State 的浅比较shouldComponentUpdate
。shouldComponentUpdate(nextProps, nextState)
(类组件): 手动实现此生命周期方法,精确控制组件是否需要重渲染。返回false
可以跳过本次更新。class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { // 仅在特定 prop 或 state 变化时才更新 return nextProps.id !== this.props.id || nextState.count !== this.state.count; } render() { /* ... */ } }
减少 render
内部计算
useMemo
: 缓存render
内部的昂贵计算结果。只有当依赖项变化时才重新计算。function MyComponent({ list }) { const expensiveCalculation = useMemo(() => { // 对 list 进行复杂处理 return list.map(i => i * 2).join(','); }, [list]); // 仅当 list 变化时重新计算 return <div>{expensiveCalculation}</div>; }
useCallback
: 缓存传递给子组件的回调函数引用。这对于依赖回调函数作为React.memo
或useEffect
依赖项的子组件优化至关重要。function Parent() { const [count, setCount] = useState(0); // 使用 useCallback 缓存 handleClick 函数引用 const handleClick = useCallback(() => { console.log('Button clicked!'); }, []); // 空依赖数组表示函数永不改变 return <Child onClick={handleClick} />; } const Child = React.memo(({ onClick }) => { /* ... */ });
调试渲染
- React DevTools Profiler: 是分析组件渲染次数、耗时以及找出性能瓶颈的官方利器。
console.log
: 在函数组件体或类组件render
方法开头添加日志,可以直观地看到组件何时被重渲染。
掌握 render
的触发机制和优化手段,是编写高性能 React 应用的基础。