为何useLayoutEffect在浏览器渲染前触发却能获取更新后DOM样式?
React中useLayoutEffect提前"读取到DOM变更"的原因解析
问题场景
我实现了一个包含SVG元素的React组件,绑定了onClick切换颜色的逻辑。过程中发现useLayoutEffect在屏幕上的DOM实际更新前,就能读取到元素的新样式状态,对此存在疑问,想搞清楚背后的原因,以及如何让它"读取到实际渲染到屏幕的DOM"。
执行流程
- 点击组件触发onClick处理函数,调用
useState修改color状态变量 color状态变化后,触发useLayoutEffect和useEffect钩子执行useLayoutEffect内通过同步sleep函数阻塞脚本执行3秒,期间打印的DOM元素style.fill已经是新的颜色值,但屏幕上的元素颜色还没变化- 3秒阻塞结束后,浏览器才完成DOM渲染,元素颜色更新为新值
核心疑问
为什么useLayoutEffect在DOM实际渲染到屏幕前,就能读取到元素的新样式?
组件代码
const sleep = (duration: number) => { let start = new Date().getTime(); let end = start; while (end < start + duration) { end = new Date().getTime(); } }; const Logo = () => { const [color, setColor] = useState<string>("red"); const logoRef = useRef<HTMLDivElement>(null); useLayoutEffect(() => { console.log(`useLayoutEffect BEFORE sleep(3sec) getElementById color=${document?.getElementById("logowrapper")?.style.fill}`); console.log(`useLayoutEffect BEFORE sleep(3sec) logoRef color=${logoRef.current?.style.fill}`); sleep(3000); console.log(`useLayoutEffect AFTER sleep(3sec) getElementById color=${document?.getElementById("logowrapper")?.style.fill}`); console.log(`useLayoutEffect AFTER sleep(3sec) logoRef color=${logoRef.current?.style.fill}`); }, [color]); useEffect(() => { console.log(`useEffect color=${document?.getElementById("logowrapper")?.style.fill}`); }, [color]); const changeColor = () => { setColor((color): string => (color === "red" ? "green" : "red")); }; return ( <div onClick={() => changeColor()} className="App-logo" style={{ fill: color }} id="logowrapper" ref={logoRef} > <LogoSvg /> </div> ); };
原因解析
这是由React的更新机制和useLayoutEffect的执行时机决定的:
- React的DOM更新步骤:当
color状态变化时,React会先计算出DOM需要更新的内容,然后直接修改DOM节点的属性(比如这里的style.fill),这一步是同步完成的,DOM节点的属性已经变成新值,但浏览器还没来得及把这个变化渲染到屏幕上。 - useLayoutEffect的执行时机:它会在React修改完DOM之后、浏览器开始重排重绘之前执行。所以此时你读取到的是React已经修改后的DOM属性值,但屏幕上的视觉变化要等浏览器完成重绘才会显示。
- 同步阻塞的影响:你用的
sleep是同步阻塞主线程的代码,浏览器的渲染队列被卡住,直到sleep执行完毕才会处理渲染任务,所以屏幕上的颜色变化延迟了3秒,但DOM属性其实早就被React修改了。
如何"看到"实际渲染的DOM
如果需要等待浏览器完成渲染后再读取DOM状态,应该使用useEffect——它的执行时机是在浏览器完成重绘之后,你可以看到代码里的useEffect打印日志的时机,正好是屏幕颜色变化之后。
另外注意:实际开发中绝对不要用这种同步阻塞主线程的sleep函数,会导致页面完全卡死,如需延迟操作,应该用setTimeout这类异步API。
内容的提问来源于stack exchange,提问作者Artem Reva
相关产品推荐
相关产品推荐

