react渲染机制

26 阅读6分钟

React 渲染 是将组件状态转换为用户界面的过程。这不是简单的 DOM 操作,而是一个声明式的、可预测的状态到 UI 的转换。

react渲染主要有两个阶段,rendercommit

render阶段是React通过Diff算法计算出哪些部分需要更新,并打上标签(effectTag),同时构建一棵新的Fiber树(workInProgress树)。这个过程是异步的,可以被中断。React会根据任务的优先级来调度任务,高优先级的任务可以打断低优先级的任务。在这个阶段,React会执行组件的渲染(调用render方法或函数组件本身)并得到React元素(虚拟DOM),然后与旧的Fiber树进行比较,生成新的Fiber树。这个过程是纯JS计算,不会产生任何副作用(如DOM更新),因此可以被中断和恢复。

commit阶段将Render阶段计算出的变更应用到真实的DOM上。这个过程是同步的,不可中断的。因为一旦开始更新DOM,就必须连续完成,否则可能会导致UI不一致。在这个阶段,React会执行生命周期方法(如componentDidMount、componentDidUpdate)和Hook(如useLayoutEffect)等,然后更新DOM。之后,React会执行一些副作用清理和调度后续的副作用(如useEffect)

React 渲染生命周期

触发更新 → 渲染阶段 → 提交阶段 → 绘制阶段
       ↓         ↓          ↓         ↓
   状态变化  计算差异   更新DOM  浏览器渲染

触发渲染的 4 种情况

// 1. 初始渲染
ReactDOM.createRoot(root).render(<App />);

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

// 3. 父组件渲染(默认行为)
function Parent() {
  return <Child />; // Parent渲染时,Child也会渲染
}

// 4. Context 变化
const value = useContext(MyContext); // Context更新时,消费者重新渲染

渲染阶段 (Render) --创建虚拟 DOM

虚拟 DOM

// 虚拟dom并不是真正的DOM,而是轻量级JavaScript对象
const virtualDOMElement = {
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello' }
      }
    ]
  }
};

// React元素实际上是这样的对象
const element = <div className="container"><h1>Hello</h1></div>;

协调(Reconciliation)

// React如何比较新旧虚拟DOM?
function reconcile(oldTree, newTree) {
  // 1. 类型不同 → 完全替换
  if (oldTree.type !== newTree.type) {
    return { action: 'REPLACE', node: newTree };
  }
  
  // 2. 类型相同 → 更新属性
  const propChanges = diffProps(oldTree.props, newTree.props);
  
  // 3. 递归比较子节点
  const childUpdates = reconcileChildren(oldTree.children, newTree.children);
  
  return { action: 'UPDATE', propChanges, childUpdates };
}

// 关键优化:Diffing策略
// - 同层比较(不会跨层级移动组件)
// - key属性帮助识别元素(列表渲染必备)

key

// 使用索引作为key,如果是渲染列表,会导致多次回流,导致页面的性能大幅下降
{items.map((item, index) => (
  <ListItem key={index} item={item} />
  // 问题:重新排序时,React会混淆组件实例
))}

// 使用唯一ID
{items.map(item => (
  <ListItem key={item.id} item={item} />
  // React可以正确识别元素,复用DOM节点
))}

// 没有key时,React会警告,性能下降
// key就是最大的作用就是为了减少对dom的操作

提交阶段(Commit)--更新真实 DOM

React 18+ 的自动批处理

// React 17及以前:只在React事件处理中批处理
function handleClick() {
  setCount(c => c + 1);  // 触发重新渲染
  setFlag(f => !f);      // 触发重新渲染
  // 结果:2次渲染(在React 17中)
}

// React 18:自动批处理所有更新
function handleClick() {
  setCount(c => c + 1);  
  setFlag(f => !f);      
  // 结果:1次渲染(合并批处理)
}

// 强制同步刷新(特殊情况)
flushSync(() => {
  setCount(c => c + 1);  // 立即渲染
});

浏览器渲染时机

// React更新DOM后,浏览器还需要做这些事:
1. 样式计算(Recalculate Style2. 布局(Layout/Reflow3. 绘制(Paint4. 合成(Composite// React尽量最小化触发布局抖动
function BadExample() {
  const [width, setWidth] = useState(100);
  
  useEffect(() => {
    // 连续读取和写入DOM,导致布局抖动
    const element = document.getElementById('box');
    const currentWidth = element.offsetWidth; // 读取(强制同步布局)
    element.style.width = `${currentWidth + 10}px`; // 写入(触发布局)
  }, []);
  
  return <div id="box" style={{ width }}>Box</div>;
}

组件渲染优化

React.memo - 组件缓存

// 普通组件:父组件渲染就会重新渲染
const ExpensiveComponent = ({ data }) => {
  console.log('渲染了!'); // 频繁打印
  return <div>{data}</div>;
};

// 使用React.memo:浅比较props
const OptimizedComponent = React.memo(({ data }) => {
  console.log('只有data变化时才渲染');
  return <div>{data}</div>;
});

// 自定义比较函数(深度比较不推荐)
const DeepEqualComponent = React.memo(
  ExpensiveComponent,
  (prevProps, nextProps) => {
    // 返回true表示"跳过渲染"
    return _.isEqual(prevProps, nextProps);
  }
);

useMemo & useCallback - 值/函数缓存

function Parent({ items }) {
  const [count, setCount] = useState(0);
  
  // 每次渲染都创建新函数
  const handleClick = () => console.log('clicked');
  
  // 缓存函数(依赖项变化时才重新创建)
  const memoizedHandleClick = useCallback(() => {
    console.log('clicked');
  }, []); // 空数组 = 只创建一次
  
  // 每次渲染都重新计算
  const expensiveValue = items.filter(item => item.active).sort();
  
  // 缓存计算结果
  const memoizedValue = useMemo(() => {
    return items.filter(item => item.active).sort();
  }, [items]); // items变化时才重新计算
  
  return <Child onClick={memoizedHandleClick} data={memoizedValue} />;
}

状态提升 vs 状态下沉

// 状态提升:共享状态放在最近的共同祖先
function App() {
  const [user, setUser] = useState(null);
  
  return (
    <Layout>
      <Header user={user} />
      <Content user={user} />
      <Sidebar user={user} /> {/* 不必要的重新渲染 */}
    </Layout>
  );
}

// 状态下沉:状态放在需要的地方
function App() {
  return (
    <Layout>
      <Header /> {/* Header自己管理user状态 */}
      <Content />
      <Sidebar />
    </Layout>
  );
}

// Context优化:分割Context
const UserContext = createContext();
const SettingsContext = createContext();

// 避免单个大Context导致所有消费者重新渲染

并发渲染(React 18+)

特性

// 传统渲染:同步、不可中断
function oldRender() {
  // 如果组件树很大,会阻塞主线程
  renderComponentTree();
  // 用户交互会卡顿
}

// 并发渲染:可中断、优先级调度
function concurrentRender() {
  // React把渲染工作分成小任务
  // 可以暂停渲染来处理高优先级更新
  // 然后继续之前的工作
}

// 使用并发特性
import { startTransition, useDeferredValue } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query); // 延迟更新
  
  // 紧急更新:用户输入
  function handleChange(e) {
    setQuery(e.target.value); // 立即更新输入框
  }
  
  // 非紧急更新:搜索结果
  function handleSearch(newQuery) {
    startTransition(() => {
      setQuery(newQuery); // 标记为非紧急更新
    });
  }
  
  return (
    <>
      <input value={query} onChange={handleChange} />
      <SearchResults query={deferredQuery} /> {/* 使用延迟值 */}
    </>
  );
}

性能分析工具

React DevTools

// 1. 组件树检查
// 2. Profiler 录制分析
// 3. 高亮更新组件

// Chrome DevTools 中的使用步骤:
// 1. 打开 Profiler 标签
// 2. 点击录制
// 3. 执行用户交互
// 4. 停止录制,分析火焰图

实际性能检查清单

// 渲染时自问这些问题:
function Component() {
  // 1. 这个渲染是必要的吗?
  // 2. 计算是否过于昂贵?
  // 3. 子组件是否会不必要地重新渲染?
  // 4. 是否有布局抖动?
  // 5. 是否可以使用并发特性优化?
  
  return <div>...</div>;
}

常见问题

1.不必要的对象创建

// 每次渲染都创建新对象
function Component({ style }) {
  return <div style={{ color: 'red', ...style }} />;
}

// 使用useMemo或提取常量
const defaultStyle = { color: 'red' };
function Component({ style }) {
  const mergedStyle = useMemo(() => ({
    ...defaultStyle,
    ...style
  }), [style]);
  
  return <div style={mergedStyle} />;
}

2.依赖项数组问题

// 忘记依赖项
useEffect(() => {
  fetchData(id);
}, []); // id变化时不会重新获取,只会初始渲染一次

//  依赖项过多导致频繁运行
useEffect(() => {
  // 每次props变化都运行
}, [props]); // props是一个对象,每次都是新的引用

// 精确指定依赖项
useEffect(() => {
  fetchData(id);
}, [id]); // 只有id变化时才运行

//场景
  const [miscellaneous, setMiscellaneous] = useState([])

  useEffect(() => {
    const fetchMiscellaneous = async () => {
      try {
        const res = await useMiscellaneous.getMiscellaneousList()
        const data = res.reverse()
        setMiscellaneous(data)