flushSync

flushSync 强制 React 同步执行状态更新,而不是批处理或延迟。

核心概述

⚠️ 痛点: React 异步批处理导致状态不同步

  • • React 自动批处理多个状态更新以提高性能
  • • 状态更新是异步的,无法立即获取最新值
  • • DOM 操作需要最新状态但 React 还未更新 DOM
  • • 第三方库集成需要同步更新(如 D3、Chart.js)

✅ 解决方案: flushSync 强制同步更新

  • 立即执行更新: 强制 React 同步渲染
  • 获取最新 DOM: 确保 DOM 已更新
  • 打破批处理: 不等待其他更新
  • 慎用: 仅在必要时使用,会影响性能

💡 心智模型: 紧急通道

将 flushSync 想象成"紧急通道":

  • 正常流程: React 批处理优化(类似普通通道)
  • 紧急情况: 需要立即更新时使用 flushSync
  • 优先通过: 跳过批处理队列,立即执行
  • 成本较高: 频繁使用会影响性能

技术规格

类型签名

import { flushSync } from 'react-dom';

function flushSync<R>(fn: () => R): R

参数说明

参数类型说明
fn() => R包含状态更新的回调函数

⚠️ 注意: flushSync 仅在 ReactDOM 的 concurrent 模式下有效。如果你使用的是 React 18 的 createRoot,默认启用 concurrent 模式。

实战演练

示例 1: 基础用法

import { flushSync } from 'react-dom';
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // ❌ 普通更新 - 异步批处理
    setCount(count + 1);
    console.log(count); // 仍然是旧值: 0

    // ✅ 使用 flushSync - 同步更新
    flushSync(() => {
      setCount(c => c + 1);
    });
    console.log(count); // 新值: 1
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

示例 2: DOM 测量

import { flushSync } from 'react-dom';
import { useState, useRef } from 'react';

function MeasureExample() {
  const [width, setWidth] = useState(0);
  const divRef = useRef<HTMLDivElement>(null);

  const expandAndMeasure = () => {
    // ❌ 错误: DOM 还未更新
    setWidth(300);
    console.log(divRef.current?.offsetWidth); // 旧值

    // ✅ 正确: 使用 flushSync
    flushSync(() => {
      setWidth(300);
    });

    // 现在 DOM 已更新
    console.log(divRef.current?.offsetWidth); // 新值: 300
  };

  return (
    <div>
      <div
        ref={divRef}
        style={{ width: `${width}px`, transition: 'width 0.3s' }}
      >
        内容区域
      </div>
      <button onClick={expandAndMeasure}>
        展开并测量
      </button>
    </div>
  );
}

示例 3: 第三方库集成(D3.js)

import { flushSync } from 'react-dom';
import { useState, useEffect, useRef } from 'react';
import * as d3 from 'd3';

function D3Chart({ data }: { data: number[] }) {
  const svgRef = useRef<SVGSVGElement>(null);

  useEffect(() => {
    // ✅ 确保 DOM 更新后再绘制 D3 图表
    flushSync(() => {
      // 这里不需要,因为我们已经在 useEffect 中
      // 但在某些情况下,可能需要强制同步
    });

    // 绘制 D3 图表
    const svg = d3.select(svgRef.current);
    svg
      .selectAll('circle')
      .data(data)
      .enter()
      .append('circle')
      .attr('cx', (d, i) => i * 50)
      .attr('cy', d => d)
      .attr('r', 10);
  }, [data]);

  return <svg ref={svgRef} width={500} height={300} />;
}

// 使用场景:
// - D3.js 图表
// - Chart.js 图表
// - 测量 DOM 尺寸
// - 滚动到新元素
// - 与第三方动画库集成

避坑指南

❌ 错误 1: 滥用 flushSync

// ❌ 错误: 不必要地使用 flushSync
function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    flushSync(() => {
      // 每次输入都强制同步更新
      setName(e.target.value);
    });
  };

  return <input onChange={handleChange} />;
}

// 问题:
// 1. 失去批处理优化
// 2. 性能严重下降
// 3. 输入卡顿
// 4. 不必要的同步渲染

✅ 正确 1: 只在必要时使用

// ✅ 正确: 大多数情况使用普通更新
function Form() {
  const [name, setName] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // 普通更新 - React 自动批处理
    setName(e.target.value);
  };

  return <input onChange={handleChange} />;
}

// ✅ 正确: 只在需要立即 DOM 时使用 flushSync
function ScrollToElement() {
  const [items, setItems] = useState([1, 2, 3]);

  const addItemAndScroll = () => {
    const newItem = items.length + 1;

    // 需要立即获取新添加元素的 DOM
    flushSync(() => {
      setItems([...items, newItem]);
    });

    // 现在 DOM 已更新,可以滚动
    const element = document.getElementById(`item-${newItem}`);
    element?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <div>
      <button onClick={addItemAndScroll}>添加并滚动</button>
      {items.map(item => (
        <div key={item} id={`item-${item}`}>
          Item {item}
        </div>
      ))}
    </div>
  );
}

❌ 错误 2: 在 flushSync 中执行副作用

// ❌ 错误: 在 flushSync 中有副作用
function Component() {
  const [data, setData] = useState(null);

  const handleClick = () => {
    flushSync(() => {
      setData(newData);
      // 副作用: API 调用
      fetch('/api/log').then(() => {
        console.log('logged');
      });
    });
  };

  return <button onClick={handleClick}>更新</button>;
}

// 问题:
// 1. 副作用执行时机不确定
// 2. 可能导致重复调用
// 3. 违反 React 设计原则

✅ 正确 2: 在 flushSync 后执行副作用

// ✅ 正确: 先更新,再执行副作用
function Component() {
  const [data, setData] = useState(null);

  const handleClick = () => {
    // 1. 强制同步更新状态
    flushSync(() => {
      setData(newData);
    });

    // 2. DOM 更新后执行副作用
    fetch('/api/log').then(() => {
      console.log('logged');
    });
  };

  return <button onClick={handleClick}>更新</button>;
}

// 或者使用 useEffect
function Component() {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (data) {
      // 数据更新后执行副作用
      fetch('/api/log', {
        method: 'POST',
        body: JSON.stringify(data)
      });
    }
  }, [data]);

  return <button onClick={() => setData(newData)}>更新</button>;
}

❌ 错误 3: 嵌套使用 flushSync

// ❌ 错误: 嵌套 flushSync
function Component() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const handleClick = () => {
    flushSync(() => {
      setA(1);

      flushSync(() => {
        // 嵌套 flushSync - 可能导致问题
        setB(2);
      });
    });
  };

  return <button onClick={handleClick}>更新</button>;
}

// 问题:
// 1. React 会发出警告
// 2. 可能导致死循环
// 3. 性能严重下降

✅ 正确 3: 批量更新后使用一次 flushSync

// ✅ 正确: 批量更新
function Component() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  const handleClick = () => {
    flushSync(() => {
      setA(1);
      setB(2);
      // 所有更新在一次 flushSync 中
    });
  };

  return <button onClick={handleClick}>更新</button>;
}

// 优点:
// 1. 只强制同步一次
// 2. 性能更好
// 3. React 不会警告

最佳实践

1. 使用场景检查清单

// 使用 flushSync 前先检查:
// ✅ 真的需要立即获取 DOM 吗?
// ✅ 是否可以重构为异步?
// ✅ 是否可以用 useEffect 替代?
// ✅ 是否可以用 ref 替代?

// 常见合理使用场景:

// 1. DOM 测量
function measure() {
  flushSync(() => setState(1));
  const rect = element.getBoundingClientRect();
}

// 2. 滚动到新元素
function scroll() {
  flushSync(() => addItem(newItem));
  document.getElementById(newItem)?.scrollIntoView();
}

// 3. 第三方库集成(D3, Chart.js)
function drawChart() {
  flushSync(() => setData(chartData));
  d3.select(svg).selectAll('circle').data(data);
}

// 4. 同步打印/export
function print() {
  flushSync(() => setContent(printContent));
  window.print();
}

2. 性能考虑

// 性能对比

// ❌ 差: 频繁使用 flushSync
function BadPerformance() {
  const [value, setValue] = useState(0);

  const handleChange = (e) => {
    // 每次输入都强制同步 - 非常慢!
    flushSync(() => {
      setValue(e.target.value);
    });
  };

  return <input value={value} onChange={handleChange} />;
}

// ✅ 好: 使用普通更新 + useEffect
function GoodPerformance() {
  const [value, setValue] = useState(0);

  const handleChange = (e) => {
    // 普通更新 - React 批处理优化
    setValue(e.target.value);
  };

  useEffect(() => {
    // DOM 更新后执行操作
    console.log('DOM updated:', value);
  }, [value]);

  return <input value={value} onChange={handleChange} />;
}

// 性能差异:
// - 普通: 批处理,每秒渲染 1-2 次
// - flushSync: 每次输入都渲染,每秒渲染数十次

3. 替代方案

需求不推荐推荐
DOM 更新后执行操作flushSyncuseEffect + ref
获取最新状态flushSyncsetState(prev => prev + 1)
DOM 测量flushSyncuseLayoutEffect
第三方库集成flushSyncuseEffect + ref

相关链接