先记住这个答案
flushSync 可以强制 React 同步处理回调中的更新,使回调结束并返回后相关 DOM 已可供后续同步代码使用。它主要用于与浏览器或第三方命令式接口衔接,不应成为普通 setter 的默认包装。为了完成这次同步工作,React 还可能冲刷其他待处理更新、Effect,或让挂起的内容重新显示 fallback。它改变的是提交时机,不会重绑定当前事件处理器捕获的 state;因此同步之后读取旧变量仍可能得到原值。
- 调用返回后读取的是已经更新的 DOM
- 闭包快照不会因同步提交而被改写
- 可能连带处理其他工作并增加交互成本
为什么 setter 下一行的 ref 可能还是空
条件输入框尚未挂载时,普通 setOpen(true) 只请求后续渲染,紧接着访问 inputRef.current 可能还没有节点。若当前集成要求在这个调用栈里拿到节点,可以把有限更新放入 flushSync,再在外部读取 ref。
下面点击编辑后,输入框会在同步边界内出现,后面的 focus 才能操作它。示例把动作写在事件处理器中,不在渲染函数或 Effect 执行期间强行要求 React 再开始一次同步渲染。
import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';
export default function InlineEditor() {
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
function edit() {
flushSync(() => setOpen(true));
inputRef.current?.focus();
}
return <>
<button onClick={edit}>编辑标题</button>
{open && <input ref={inputRef} aria-label="标题" defaultValue="面试笔记" />}
</>;
}点击后可以立即对新挂载的输入框聚焦。如果只是希望节点出现时执行聚焦,可考虑回调 ref;不要为了省去生命周期设计而给每一个普通状态更新加同步包装。
强制提交不会改写事件里的局部变量
即使同步之后页面已经显示输入框,edit 函数中捕获的 open 仍是进入这次处理器时的值。要验证当前 DOM 可以读取 ref 或节点状态;要表达下一状态,可以使用明确的局部 next,而不是期待旧变量被框架原地修改。
同样,flushSync 接收的回调不是可等待整个异步业务的事务。若回调里启动 Promise,后续异步完成的更新不会因为曾位于这个函数定义中,就自动全部获得同一次同步提交保证。应把同步范围保持清晰且短小。
成本可能超过回调里看起来很少的代码
一个小 setter 也可能影响较大的子树,React 为了冲刷它还可能处理回调之外必要的待执行工作。Effect 及其安排的更新可能提前运行,Suspense 边界也可能重新显示加载内容,不能只按回调行数估算成本。
在滚动、输入等高频事件中频繁使用会压缩浏览器处理交互和绘制的空间。先用正常状态与提交后的动作完成需求,只有外部同步契约确实要求时才保留,并通过真实交互检查页面是否出现多余提交、闪烁或明显延迟。
容易答错的地方
- flushSync 是修复所有 state 旧值的通用方法
- 它同步 DOM 提交,不改变 JavaScript 闭包绑定。旧变量问题应从快照和更新函数解释,不能靠强制渲染掩盖数据读取语义。
- 回调里只有一个 setter,所以没有额外代价
- 更新可能传播到更大子树,还可能连带冲刷其他待处理工作;应根据实际提交和交互测量,不能用源代码行数判断性能成本。
面试官还会怎么问?
为什么不直接给新输入框使用 autoFocus?
对于简单的首次出现聚焦,声明式属性或回调 ref 可能足够。flushSync 更适合外部同步流程确实要立即继续读取 DOM 的情况,不应为了示例而扩大使用。
可以在 useEffect 里面直接调用 flushSync 吗?
不应在 React 已经执行生命周期工作的过程中这样要求同步冲刷。需要重新设计触发位置或使用正常更新,避免把同步边界嵌进正在进行的渲染流程。
flushSync 返回是否说明远程保存也成功了?
不说明,它只涉及 React 的本地更新与 DOM 提交。网络动作有独立的完成、失败和副作用状态,需要通过请求结果或业务查询确认。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。