文章目录

React 18的自动批处理与useTransition

发布于 2026-05-30 22:19:46 · 浏览 32 次 · 评论 0 条

React 18的自动批处理与useTransition

React 18 带来了多项旨在提升应用性能和用户体验的更新。其中,自动批处理 (Automatic Batching)useTransition 是两个核心特性。本文将指导你理解它们的工作原理,并在代码中正确应用。


第一部分:理解并利用自动批处理

在React 18之前,状态更新批处理仅发生在React的事件处理函数内部。在setTimeoutPromise、原生事件处理等异步操作中,每次setState都会触发一次单独的渲染,这可能导致不必要的性能开销。

1. 识别问题(React 17)

假设你有一个按钮,点击后在setTimeout里同时更新两个状态变量。

// React 17 行为示例
import React, { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setTimeout(() => {
      setCount(c => c + 1); // 第一次渲染
      setFlag(f => !f);     // 第二次渲染
    }, 0);
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? 'blue' : 'black' }}>{count}</h1>
    </div>
  );
}

点击按钮,React 17 会触发两次独立的渲染。这不仅效率低下,还可能引发中间状态的视觉闪烁。

2. React 18的自动批处理

React 18 将批处理能力扩展到了所有状态更新,包括setTimeoutPromise、原生事件处理等。

无需修改任何代码,上面例子中的两次setState在React 18下会被自动合并为一次渲染,性能得到提升。

3. 需要同步更新时怎么办?

在极少数情况下,你可能需要在事件处理外部立即应用状态更新(例如,读取DOM更新后的值)。React 18 提供了 flushSync API 来退出批处理。

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // 此时DOM已更新
  flushSync(() => {
    setFlag(f => !f);
  });
  // 此时DOM再次更新
}

注意flushSync 会强制同步渲染,应谨慎使用,它可能对性能产生负面影响。

对比总结

场景 React 17 行为 React 18 行为
事件处理器 (onClick等) 批处理 自动批处理
setTimeout/setInterval 回调 非批处理(多次渲染) 自动批处理
Promise 回调 非批处理 自动批处理
原生事件处理器 非批处理 自动批处理
flushSync 中的更新 (不适用) 同步、非批处理

第二部分:掌握useTransition处理长任务

有时,状态更新会触发复杂的计算或渲染,导致用户界面卡顿useTransition 允许你将某些状态更新标记为 “过渡” ,从而让React能够优先处理更紧急的更新(如用户输入),并非阻塞地处理“过渡”更新。

1. useTransition的基本用法

useTransition 是一个React Hook,返回一个数组。

const [isPending, startTransition] = useTransition();
  • isPending: 一个布尔值,指示过渡状态是否仍在进行中。你可以用它来显示加载指示器。
  • startTransition: 一个函数,你用它来包裹会导致性能瓶颈的状态更新

2. 实践场景:过滤大型列表

想象一个包含10,000个项目的列表,用户在输入框中输入文本进行过滤。直接设置过滤状态会导致输入卡顿,因为重新渲染整个列表是耗时操作。

步骤1:创建组件并设置初始状态

import React, { useState, useTransition } from 'react';

function FilterableList() {
  const [input, setInput] = useState('');
  const [list, setList] = useState(generateLargeList(10000)); // 生成大型列表
  const [isPending, startTransition] = useTransition();
  const [filterTerm, setFilterTerm] = useState('');

  // 根据filterTerm过滤列表
  const filteredList = list.filter(item =>
    item.toLowerCase().includes(filterTerm.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={handleChange}
      />
      {isPending && <p>正在加载...</p>}
      <ul>
        {filteredList.map(item => <li key={item}>{item}</li>)}
      </ul>
    </div>
  );
}

步骤2:处理输入变化,使用startTransition

function handleChange(e) {
  // 紧急更新:立即更新输入框的值,保持输入响应
  setInput(e.target.value);

  // 过渡更新:将列表过滤这个可能耗时的操作放入transition
  startTransition(() => {
    setFilterTerm(e.target.value);
  });
}

关键点分析

  1. setInput 是紧急更新。React会立即处理它,确保输入框字符跟得上用户的打字速度。
  2. setFilterTermstartTransition 内部。这是一个过渡更新。React知道这个更新优先级较低。
  3. 如果用户快速连续输入,新的紧急输入更新会中断正在进行的列表过滤渲染。React会先渲染最新的输入值,然后重新开始过滤过程以匹配最新输入。这避免了输入卡顿。
  4. isPending 在过滤渲染进行时为 true,你可以据此显示一个加载状态。

3. useTransition与useDeferredValue的比较

React 18 还提供了另一个相关的Hook:useDeferredValue。它可以看作是 useTransition 的一个便捷“包装器”。

const deferredFilterTerm = useDeferredValue(filterTerm);
const filteredList = list.filter(item =>
  item.toLowerCase().includes(deferredFilterTerm.toLowerCase())
);

useDeferredValue返回一个延迟版本的值。在内部,它类似于用 startTransition 更新一个值。当原始值(filterTerm)变化时,deferredFilterTerm 不会立即更新,而是会与当前渲染“脱离”,允许紧急渲染(如输入框更新)先进行。

如何选择

  • 当你需要控制一个状态更新的开始时机,或者需要知道过渡状态是否活跃isPending)时,使用 useTransition
  • 当你有一个需要延迟渲染,且这个延迟是根据组件的其他状态(如输入)自然发生的,使用 useDeferredValue 更简单。

整合应用

将自动批处理与useTransition结合,可以构建出高度响应、无卡顿的交互界面。

import React, { useState, useTransition, useEffect } from 'react';

function SearchApp() {
  const [input, setInput] = useState('');
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  // 模拟搜索API调用
  useEffect(() => {
    if (query) {
      // 实际项目中,这里会是真实的API请求
      const timer = setTimeout(() => {
        setResults(generateMockResults(query));
      }, 500);
      return () => clearTimeout(timer);
    } else {
      setResults([]);
    }
  }, [query]);

  const handleSearch = (e) => {
    // 紧急更新:保持输入框响应
    setInput(e.target.value);

    // 过渡更新:触发搜索
    startTransition(() => {
      setQuery(e.target.value);
    });
  };

  return (
    <div>
      <input value={input} onChange={handleSearch} placeholder="搜索..." />
      {isPending && <div>搜索中...</div>}
      <ul>
        {results.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

在这个例子中,setInput是紧急更新,setQuery是过渡更新。当用户快速输入时,输入框始终保持流畅,而搜索和结果渲染在后台非阻塞地进行,并通过isPending显示加载状态。

graph TD A[用户快速输入] --> B{React 18 调度器}; B --> C[紧急更新: setInput]; C --> D[立即渲染输入框]; B --> E[过渡更新: setQuery in startTransition]; E --> F{中断检查}; F -- 无新输入 --> G[执行搜索 & 渲染列表]; F -- 新输入到达 --> H[中止当前过渡渲染]; H --> B; G --> I[UI更新: 显示结果];

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文