深度解析React18 flushSync的使用

 更新时间:2026年06月18日 10:18:01   作者:英俊潇洒美少年  
React18的flushSync API用于强制同步执行状态更新和DOM渲染,跳过React的并发调度和批处理机制,文章全面解析了flushSync的核心作用、基础用法、适用场景和禁止使用场景,感兴趣的可以了解一下

前言

React18 引入了并发渲染与全局自动批处理,默认情况下所有setState都会异步合并渲染优化性能。但部分业务需要修改状态后立刻拿到最新DOM,官方提供了flushSync强制同步刷新API。本文覆盖所有执行场景、陷阱、优先级、异常、对比、面试题

导入方式:import { flushSync } from 'react-dom'

一、基础介绍

1. 核心作用

强制同步执行回调内状态更新,跳过并发调度、批处理机制,同步走完render → commit → DOM真实更新全流程。

2. 基础示例

const [count, setCount] = useState(0)
const handle = () => {
  flushSync(() => {
    setCount(1)
  })
  // 此处DOM已经刷新完成,可读取offset、焦点等
}

3. 适用场景

  1. 修改状态后立即读取DOM宽高、位置、滚动距离
  2. 渲染完成马上控制输入框焦点
  3. 对接图表、动画等第三方库,必须依赖最新DOM实例
  4. 多步骤逻辑强依赖上一步渲染结果

4. 禁止使用场景

  1. 组件渲染函数体内直接调用
  2. useEffectuseLayoutEffect内部调用(会控制台警告)
  3. 无必要场景滥用,会破坏并发渲染、降低页面性能

二、全场景执行逻辑拆解

场景1:flushSync前置存在普通setState

规则

执行flushSync时,会优先清空之前所有堆积未渲染的更新队列,前置更新单独渲染一次,内部更新合并再渲染一次。

const [n, setN] = useState(0)
const run = () => {
  setN(1) // 缓存入队,暂不渲染
  flushSync(() => {
    setN(prev => prev + 2)
  })
  console.log(n) // 输出3
}

执行流程:

  1. setN(1)进入待更新队列
  2. 进入flushSync,先冲刷前置队列,n=1,第一次同步渲染
  3. 内部setN(prev+2)存入同步批次
  4. 回调执行完毕批量渲染,n=3,第二次渲染

场景2:flushSync内部多个setState

规则

同一个flushSync回调内多个状态更新,自动合并批量,只同步渲染1次。区别普通更新:普通是异步批处理,这里是同步批处理。

flushSync(() => {
  setCount(1)
  setName('demo')
  setVisible(true)
})
// 三个状态一次性同步更新DOM,仅渲染一次

场景3:flushSync嵌套调用

底层铁律

同一时间只能存在一个同步刷新上下文;同步栈内嵌套的flushSync直接失效,仅执行回调,不会新增渲染次数。

flushSync(() => {
  setCount(p => p + 1)
  // 内层全部失效,无独立刷新
  flushSync(() => {
    setCount(p => p + 2)
    flushSync(() => setCount(p => p + 3))
  })
})
// 总计只渲染1次,最终count=6

嵌套问题解决方案

想要分步多次同步刷新,禁止嵌套,拆分为平级独立flushSync

flushSync(() => setA(1)) // 第一次渲染
flushSync(() => setB(2)) // 第二次渲染
flushSync(() => setC(3)) // 第三次渲染

场景4:flushSync内部包含异步代码(async/await/setTimeout/Promise)

核心结论

只有同步执行栈内的setState受flushSync管控;一旦进入异步回调(定时器、then、await后),状态更新脱离同步上下文,回归默认异步批处理。

  1. setTimeout宏任务示例
flushSync(() => {
  setNum(1)
  setTimeout(() => {
    setNum(2) // 异步队列,延迟渲染,不跟随同步刷新
  }, 0)
})
// 外层DOM先变为1,宏任务执行后才变为2
  1. async/await陷阱(极易踩坑)
const fn = async () => {
  flushSync(async () => {
    setN(1)
    await Promise.resolve() // 让出同步栈,后续代码变成微任务
    setN(2) // 不受flushSync管控,异步渲染
  })
}
  1. 异步回调内新开flushSync
    异步执行栈中创建的flushSync是全新独立同步上下文,可以正常强制刷新:
flushSync(() => {
  setNum(1)
  setTimeout(() => {
    flushSync(() => setNum(2)) // 独立生效,同步渲染
  }, 0)
})

场景5:flushSync内部抛出异常

规则

回调内报错不会回滚已执行的状态更新,已经执行的setState依旧会同步渲染;错误同步抛出,可用try/catch捕获。

const [n, setN] = useState(0)
const test = () => {
  try {
    flushSync(() => {
      setN(1) // 正常渲染生效
      throw new Error('执行异常')
      setN(2) // 不会执行
    })
  } catch (err) {}
  console.log(n) // 打印1
}

场景6:flushSync搭配startTransition(优先级对比)

优先级排序:flushSync(最高同步) > 普通更新 > startTransition(低优先级过渡)
flushSync会插队优先执行,transition内更新延后渲染:

const [n, setN] = useState(0)
const run = () => {
  startTransition(() => setN(10)) // 低优先级延后
  flushSync(() => setN(1)) // 最高优先级立刻渲染
  console.log(n) // 打印1
}

场景7:useLayoutEffect / useEffect中调用flushSync

渲染周期钩子内部调用会触发React警告,属于不规范写法。渲染阶段本身正在执行更新流程,无法二次开启同步刷新上下文。

// 警告不推荐
useLayoutEffect(() => {
  flushSync(() => setCount(1))
}, [])

场景8:实战标准DOM读取场景

业务最常用:先展示隐藏元素,立刻读取尺寸计算动画、定位:

const boxRef = useRef(null)
const openBox = () => {
  flushSync(() => setShow(true))
  // DOM已渲染完成,精准获取高度
  const height = boxRef.current.offsetHeight
}

三、经典闭包快照陷阱

flushSync仅同步DOM视图,组件内state变量为函数执行瞬间的闭包快照,不会实时变化;状态累加务必使用函数式更新setX(prev => prev + val)

const [val, setVal] = useState(0)
const run = () => {
  setVal(1)
  flushSync(() => {
    console.log(val) // 0,闭包捕获旧快照
    setVal(p => p + 2) // 函数式更新不受快照影响
  })
}

四、重要注意事项(避坑清单)

  1. 杜绝滥用:强制同步渲染会打断React并发机制,频繁调用造成页面卡顿
  2. 同步栈内禁止嵌套,分步刷新只能平级书写
  3. 不要给flushSync传递async回调,await会撕裂同步执行栈
  4. 异步逻辑如需同步DOM,在异步回调内部单独包裹flushSync
  5. 循环中批量调用flushSync会产生多次重渲染,性能损耗极大
  6. 遇到Suspense时,flushSync强制同步会直接展示fallback加载态,无法等待异步资源
  7. 状态累加统一使用函数式更新,规避闭包快照数值错误

五、flushSync vs Vue nextTick 对比

对比项React flushSyncVue nextTick
核心行为主动强制立刻同步刷新DOM被动等待DOM更新完毕再执行回调
执行类型同步阻塞代码微任务异步执行
批处理影响打破React默认自动批处理不改变Vue自身批处理逻辑
使用目的修改state马上拿到最新DOM修改数据后等渲染完成操作DOM
两者关系逻辑完全相反,不能互相替代逻辑完全相反,不能互相替代

六、高频面试真题(含完整答案)

面试题1 前置setState+内部多更新

const [n, setN] = useState(0);
function handleClick() {
  setN(1);
  flushSync(() => {
    setN(prev => prev + 2);
    setN(prev => prev + 3);
  });
  console.log(n);
}

答案:打印6;总渲染2次(前置1次、内部合并1次)

面试题2 同步多层嵌套flushSync

const [num, setNum] = useState(0);
function test() {
  flushSync(() => {
    setNum(1);
    flushSync(() => {
      setNum(2);
      flushSync(() => {
        setNum(3);
      });
    });
  });
  console.log(num);
}

答案:打印3;仅渲染1次,内层嵌套全部失效

面试题3 压轴综合大题

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

  flushSync(() => {
    setCount(p => p + 1);
    flushSync(() => {
      setCount(p => p + 2);
      flushSync(() => {
        setCount(p => p + 3);
      });
    });
  });

  flushSync(() => {
    setCount(p => p + 4);
  });

  console.log(count);
}

问题1:最终打印数值?问题2:原始代码渲染几次?问题3:第二个平级flushSync挪入最深嵌套渲染几次?
答案:

  1. 1+1+2+3+4 = 11,打印11
  2. 原始渲染3次:前置1次、嵌套批量1次、平级第二个独立1次
  3. 移入嵌套后全程单同步上下文,仅渲染2次

面试题4 异常场景

const [n, setN] = useState(0);
function test() {
  try {
    flushSync(() => {
      setN(1);
      throw Error('err');
    });
  } catch (e) {}
  console.log(n);
}

答案:打印1,异常不会回滚已执行状态

面试题5 优先级startTransition对比

const [n, setN] = useState(0);
function test() {
  startTransition(() => setN(10));
  flushSync(() => setN(1));
  console.log(n);
}

答案:打印1,flushSync优先级高于transition

七、开发最佳实践

  1. 优先使用useLayoutEffect派生数据、操作DOM,非必要不用flushSync
  2. 接口请求、异步逻辑放在flushSync外部,拿到结果后再同步刷新视图
  3. 同一批次无关多状态统一放入单个flushSync,减少渲染次数
  4. 多层分步DOM操作拆分平级flushSync,不依赖嵌套逻辑
  5. 所有数值叠加更新全部采用函数式setX(prev => prev + value)写法

总结

  1. flushSync为最高优先级同步刷新,前置队列会提前冲刷
  2. 同回调内多setState合并一次渲染;同步栈嵌套内层失效
  3. 异步代码脱离同步上下文,只有异步内新开flushSync才可再次同步
  4. 异常、闭包、transition、生命周期均有固定执行规则
  5. 和Vue nextTick行为完全相反,一个强制立刻刷,一个等待刷完再执行
  6. 谨慎使用,仅DOM读取、焦点、第三方库兼容场景作为兜底方案

到此这篇关于深度解析React18 flushSync的使用的文章就介绍到这了,更多相关React18 flushSync内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • JavaScript React如何修改默认端口号方法详解

    JavaScript React如何修改默认端口号方法详解

    这篇文章主要介绍了JavaScript React如何修改默认端口号方法详解,文中通过步骤图片解析介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • React Immutable使用方法详细介绍

    React Immutable使用方法详细介绍

    Immutable.js出自Facebook,是最流行的不可变数据结构的实现之一。它实现了完全的持久化数据结构,使用结构共享。所有的更新操作都会返回新的值,但是在内部结构是共享的,来减少内存占用
    2022-09-09
  • 如何解决React useEffect钩子带来的无限循环问题

    如何解决React useEffect钩子带来的无限循环问题

    本文主要介绍了解决React useEffect钩子带来的无限循环问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • React项目仿小红书首页保姆级实战教程

    React项目仿小红书首页保姆级实战教程

    React 是一个用于构建用户界面的 Javascript库,接下来将通过实战小红书首页的详细介绍其设计思路和方法,将读者带入到react的开源世界,需要的朋友可以参考下
    2022-07-07
  • 在React中使用SVG的几种方式

    在React中使用SVG的几种方式

    在React中,SVG(Scalable Vector Graphics)的使用非常普遍,因为它们提供了可伸缩的矢量图形,这对于现代Web应用来说是非常重要的,以下是几种常见的在React中使用SVG的方法,每种方法都有其特定的用例和最佳实践,需要的朋友可以参考下
    2024-11-11
  • react-router-domV6版本的路由和嵌套路由写法详解

    react-router-domV6版本的路由和嵌套路由写法详解

    本文主要介绍了react-router-domV6版本的路由和嵌套路由写法详解,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • 深入理解React调度(Scheduler)原理

    深入理解React调度(Scheduler)原理

    本文主要介绍了深入理解React调度(Scheduler)原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-07-07
  • 使用React18和WebSocket构建实时通信功能详解

    使用React18和WebSocket构建实时通信功能详解

    WebSocket是一种在Web应用中实现双向通信的协议,它允许服务器主动向客户端推送数据,而不需要客户端发起请求,本文将探索如何在React 18应用中使用WebSocket来实现实时通信,感兴趣的可以了解下
    2024-01-01
  • React中使用UMEditor的方法示例

    React中使用UMEditor的方法示例

    这篇文章主要介绍了React中使用UMEditor的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • React和Vue的props验证示例详解

    React和Vue的props验证示例详解

    这篇文章主要介绍了React和Vue的props验证,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08

最新评论