React 组件渲染流程

React 组件从触发更新到页面渲染的完整过程

问题

React 组件是如何渲染到页面上的?更新时经历了哪些阶段?

解答

React 渲染分为两个主要阶段:Render 阶段Commit 阶段

触发渲染的方式

// 1. 首次渲染
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 2. 状态更新
const [count, setCount] = useState(0);
setCount(1); // 触发重新渲染

// 3. 父组件重新渲染
// 父组件渲染时,子组件默认也会重新渲染

Render 阶段(可中断)

这个阶段 React 会构建 Fiber 树,计算出需要更新的内容:

function Counter() {
  const [count, setCount] = useState(0);
  
  // 每次渲染,函数组件会重新执行
  console.log('render'); // 状态变化时会打印
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Render 阶段的工作:

  1. 调用组件函数/render 方法,获取新的 JSX
  2. Diff 算法比较新旧虚拟 DOM
  3. 标记变更,记录需要执行的 DOM 操作
// React 内部会将 JSX 转换为虚拟 DOM 对象
// <div className="box">hello</div>
// 转换为:
{
  type: 'div',
  props: {
    className: 'box',
    children: 'hello'
  }
}

Commit 阶段(不可中断)

将 Render 阶段计算出的变更应用到真实 DOM:

function Example() {
  useEffect(() => {
    // 3. useEffect 回调(异步执行)
    console.log('useEffect');
  });

  useLayoutEffect(() => {
    // 2. useLayoutEffect 回调(DOM 更新后同步执行)
    console.log('useLayoutEffect');
  });

  // 1. 渲染
  console.log('render');
  
  return <div>Example</div>;
}

// 输出顺序:render -> useLayoutEffect -> useEffect

Commit 阶段分为三个子阶段:

  1. Before Mutation:执行 getSnapshotBeforeUpdate
  2. Mutation:执行 DOM 操作(增删改)
  3. Layout:执行 useLayoutEffect,更新 ref

完整流程示例

import { useState, useEffect, memo } from 'react';

// 使用 memo 避免不必要的重新渲染
const Child = memo(function Child({ name }) {
  console.log('Child render');
  return <p>Hello, {name}</p>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('React');

  console.log('Parent render');

  useEffect(() => {
    console.log('Parent effect, count:', count);
  }, [count]); // 依赖数组控制 effect 执行时机

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child name={name} /> {/* name 不变,Child 不会重新渲染 */}
    </div>
  );
}

// 点击按钮后输出:
// Parent render
// Parent effect, count: 1
// (Child 不会重新渲染,因为 props 没变)

类组件的生命周期对应

class MyComponent extends React.Component {
  // Render 阶段
  static getDerivedStateFromProps() {}
  shouldComponentUpdate() {}
  render() {}

  // Commit 阶段
  getSnapshotBeforeUpdate() {}  // Before Mutation
  componentDidMount() {}         // Layout(首次)
  componentDidUpdate() {}        // Layout(更新)
  componentWillUnmount() {}      // Mutation(卸载时)
}

关键点

  • 两阶段渲染:Render 阶段可中断(计算变更),Commit 阶段不可中断(操作 DOM)
  • 触发条件:首次挂载、setState、props 变化、forceUpdate
  • Fiber 架构:将渲染工作拆分为小单元,支持时间切片和优先级调度
  • Diff 优化:同层比较、key 标识、类型判断,时间复杂度 O(n)
  • 批量更新:React 18 默认对所有更新进行批处理,减少渲染次数