Back to Blog
· 5 min read

React 批量更新笔记

React

批量更新(Batching)是 React 的一项核心优化技术,它将多个状态更新合并为一次渲染,从而提升性能,理解批量更新机制,才写出更高效的 React.

1. 批量更新

批量更新是一种将多个状态更新合并为单次渲染的机制。考虑以下场景:

function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  function handleClick() {
    setCount1(count1 + 1);
    setCount2(count2 + 1);
    // 两次状态更新,React 会合并为一次渲染
  }

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

点击按钮后,虽然调用了两次 setState,但组件只会重新渲染一次。这就是批量更新的效果。

2. React 17 及更早版本的批量更新

2.1 实现原理

// React 17 简化版实现
let isBatchingUpdates = false;
let updateQueue = [];

function batchedUpdates(callback) {
  const alreadyBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;

  try {
    return callback();
  } finally {
    isBatchingUpdates = alreadyBatchingUpdates;
    if (!isBatchingUpdates) {
      flushBatchedUpdates();
    }
  }
}

function flushBatchedUpdates() {
  while (updateQueue.length) {
    const update = updateQueue.shift();
    update.perform();
  }
}

关键点:

  • isBatchingUpdates 标志位决定是否正在批量更新
  • batchedUpdates 函数开启批量模式,执行回调后刷新队列
  • 事件处理器中 React 自动启用批量更新

2.2 局限性

在 React 17 及更早版本中,批量更新主要限于事件处理器:

// React 17: setTimeout 中的更新不会批量处理
function handleClick() {
  setTimeout(() => {
    setCount1(c => c + 1); // 触发一次渲染
    setCount2(c => c + 1); // 再触发一次渲染
  }, 1000);
}

注意: 异步回调中的状态更新不会被批量处理,每个 setState 会触发单独的渲染。


3. React 18 的自动批量更新

3.1 核心改进

React 18 引入了自动批量更新,即使在异步代码中也能合并状态更新:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  function handleAsyncUpdate() {
    setTimeout(() => {
      setCount1(c => c + 1); // 自动合并
      setCount2(c => c + 1); // 自动合并
      // 只会触发一次渲染!
    }, 1000);
  }

  return (
    <div>
      <p>Count 1: {count1}</p>
      <p>Count 2: {count2}</p>
      <button onClick={handleAsyncUpdate}>异步更新</button>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);

3.2 性能收益

场景React 17React 18
事件处理器中的多次更新1 次渲染1 次渲染
setTimeout 中的多次更新多次渲染1 次渲染
Promise.then 中的多次更新多次渲染1 次渲染
fetch 回调中的多次更新多次渲染1 次渲染

3.3 手动批量更新

如果需要在特定场景手动批量更新,可以使用 unstable_batchedUpdates

import { unstable_batchedUpdates } from 'react-dom';

function handleClick() {
  unstable_batchedUpdates(() => {
    setCount1(c => c + 1);
    setCount2(c => c + 1);
  });
}

提示: React 18 中大多数场景无需手动调用,框架会自动处理。

4. 批量更新流程图

graph TD
    A[状态更新触发] --> B{是否在批量上下文中?}
    B -->|是| C[加入更新队列]
    B -->|否| D[立即处理更新]
    C --> E{批量操作结束?}
    E -->|否| F[等待更多更新]
    E -->|是| G[执行批量渲染]
    D --> H[执行单次渲染]
    G --> I[组件重新渲染]
    H --> I

5. 与其他 React 18 特性结合

5.1 Transition API

批量更新与 startTransition 结合,可以区分紧急和非紧急更新:

import { useTransition, useState } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // 紧急更新 - 输入框立即响应

    startTransition(() => {
      // 非紧急更新 - 搜索结果批量处理
      searchAPI(value);
    });
  }
}

5.2 并发渲染

React 18 的并发渲染允许中断和恢复渲染,这是 React 的一次重大架构升级。

5.2.1 什么是并发渲染

传统 React(17 及之前)的渲染是同步的:一旦开始渲染,就会阻塞主线程,直到渲染完成。如果组件树很大,用户交互可能会被延迟,导致卡顿。

并发渲染则不同:React 可以同时准备多个版本的 UI,根据用户交互的优先级动态切换。

graph TD
    A[用户输入] --> B{紧急更新?}
    B -->|是| C[高优先级渲染]
    B -->|否| D[低优先级渲染]
    C --> E[立即响应]
    D --> F[可中断]
    F --> G[更紧急的更新到达]
    G --> C
    F --> H[完成渲染]

5.2.2 核心概念:Fiber 架构

React 18 的并发渲染基于 Fiber 架构。Fiber 是 React 内部的一种数据结构,将渲染工作拆分成小单元:

// 传统渲染:一次性完成
// 组件A → 组件B → 组件C → DOM 更新

// Fiber 渲染:分片完成
// [工作单元1] → [工作单元2] → [工作单元3] → ... → DOM 更新
//     ↑              ↓
//   中断          恢复

5.2.3 useDeferredValue

useDeferredValue 用于延迟非关键内容的渲染:

import { useDeferredValue, useState, useMemo } from 'react';

function SearchResults() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // 大量数据的筛选 - 使用延迟值
  const filteredItems = useMemo(
    () => items.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [deferredQuery]
  );

  // 样式区分:延迟渲染时显示不同状态
  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        {filteredItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  );
}

5.2.4 useTransition

startTransition 标记非紧急更新:

import { useTransition, useState } from 'react';

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [activeTab, setActiveTab] = useState('posts');

  function handleTabChange(tab) {
    // 立即更新 UI 状态
    setActiveTab(tab);

    // 但内容切换可以延迟
    startTransition(() => {
      // 这个更新会被标记为低优先级
      fetchTabData(tab);
    });
  }

  return (
    <div>
      <button onClick={() => handleTabChange('posts')}>文章</button>
      <button onClick={() => handleTabChange('comments')}>评论</button>
      <button onClick={() => handleTabChange('settings')}>设置</button>

      {isPending && <LoadingSpinner />}
      <TabContent activeTab={activeTab} />
    </div>
  );
}

5.2.5 并发渲染的工作流程

sequenceDiagram
    participant User as 用户
    participant React as React 引擎
    participant Fiber as Fiber 调度器
    participant DOM as DOM

    User->>React: 输入搜索关键词
    React->>Fiber: 创建高优先级任务
    Fiber->>Fiber: 中断低优先级渲染
    Fiber->>DOM: 优先更新输入框
    Note over Fiber: 用户看到即时反馈
    Fiber->>DOM: 完成搜索结果渲染
    Note over DOM: 显示搜索结果

5.2.6 使用场景对比

场景解决方案说明
搜索输入 + 结果列表useDeferredValue输入即时响应,结果延迟渲染
标签页切换useTransition切换按钮立即响应,内容可延迟
大列表滚动useDeferredValue滚动时保持流畅
表单验证无需处理验证通常是紧急的

5.2.7 注意事项

// ❌ 错误:不要对状态值使用 useDeferredValue
function BadExample() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  // 这没有意义,query 和 deferredQuery 本质相同
  return <div>{deferredQuery}</div>;
}

// ✅ 正确:deferredQuery 用于派生计算
function GoodExample() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const filteredItems = useMemo(
    () => expensiveFilter(allItems, deferredQuery),
    [deferredQuery]
  );

  return <List items={filteredItems} />;
}

5.2.8 与批量更新的关系

并发渲染和批量更新是互补的:

function App() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();

  function handleClick() {
    setCount(c => c + 1); // 高优先级更新 - 自动批量

    startTransition(() => {
      // 低优先级更新 - 可中断
      setFilterValue(newValue);
      setSearchResults(newResults);
    });
  }
}
  • 批量更新: 合并多个 setState 为一次渲染
  • 并发渲染: 根据优先级决定渲染顺序和是否中断

两者结合,React 18 能够智能地处理各种更新场景,提供流畅的用户体验。

6. 总结

React 批量更新机制经历了重要演进:

  • React 17: 仅在事件处理器中自动批量更新
  • React 18: 自动批量更新扩展到所有场景

这项改进带来了显著的性能提升,开发者无需担心状态更新的上下文,React 会智能地合并渲染。

相关阅读