React初始与后续渲染中useEffect、Promise执行优先级疑问
React中Promise、setTimeout与useEffect的执行顺序差异解析
问题现象
在CodeSandbox运行代码时得到输出:
1 2 3 4 5 6 1 2 3 4
但在bigfrontend.dev的测试中,正确输出为:
1 3 4 2 5 6 1 2 3 4
核心困惑:初始渲染时Promise、setTimeout的回调先于useEffect执行,而点击后的更新渲染顺序相反。
代码示例
'infiniteLoopProtection:false' import * as React from "react"; import { useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { screen, fireEvent } from "@testing-library/dom"; function App() { const [state, setState] = useState(0) console.log(1) const start = Date.now() while (Date.now() - start < 50) { window.timestamp = Date.now() } useEffect(() => { console.log(2) }, [state]) Promise.resolve().then(() => console.log(3)) setTimeout(() => console.log(4), 0) const onClick = () => { console.log(5) setState(num => num + 1) console.log(6) } return <div> <button onClick={onClick}>click me</button> </div> } const root = createRoot(document.getElementById('root')); root.render(<App/>) setTimeout(() => fireEvent.click(screen.getByText('click me')), 100)
执行顺序详细解释
1. 初始渲染(第一次挂载)
执行流程:
- 同步代码执行:调用
root.render(<App/>)后,App组件同步执行:- 打印
1 - 执行50ms的while循环,阻塞主线程
- 注册
useEffect回调(仅注册,此时不执行) Promise.resolve().then()将打印3的回调加入微任务队列setTimeout(() => console.log(4), 0)将打印4的回调加入宏任务队列(由于主线程被阻塞50ms,该回调的实际执行时机在阻塞结束后)
- 打印
- React commit阶段:同步完成组件DOM挂载
- 事件循环处理:
- 主线程空闲后,优先处理当前微任务队列:执行Promise回调,打印
3 - 微任务队列清空后,处理宏任务队列:执行setTimeout回调,打印
4 - 本次宏任务执行完毕后,React调度执行
useEffect的回调(挂载阶段的effect),打印2
- 主线程空闲后,优先处理当前微任务队列:执行Promise回调,打印
这就解释了初始渲染的顺序:1 → 3 → 4 → 2
2. 点击按钮后的更新渲染
执行流程:
- 点击事件同步代码:
- 打印
5 - 调用
setState,React标记组件需要更新(18版本中默认批量更新,不会立即触发渲染) - 打印
6
- 打印
- React更新渲染:
- 同步重新执行App组件,打印
1 - 注册新的
useEffect回调,同时检测到依赖state变化,标记需要执行更新后的effect - 注册Promise微任务(打印
3)和setTimeout宏任务(打印4)
- 同步重新执行App组件,打印
- React commit阶段:同步完成DOM更新
- 事件循环处理:
- React在commit阶段结束后,将
useEffect的回调优先加入微任务队列 - 处理微任务队列:先执行
useEffect回调打印2,再执行Promise回调打印3 - 微任务队列清空后,处理宏任务队列:执行setTimeout回调打印
4
- React在commit阶段结束后,将
这就解释了更新渲染的顺序:5 → 6 → 1 → 2 → 3 → 4
为何两次渲染顺序不同?
核心原因是初始渲染与更新渲染的调度时机差异:
- 初始渲染时,组件同步代码中先添加了Promise微任务,React在commit阶段后添加的useEffect微任务会排在已有微任务之后;同时由于50ms阻塞,setTimeout的宏任务已经到达执行时机,在微任务清空后立即执行,导致useEffect回调最后执行。
- 更新渲染时,React在commit阶段后会优先调度useEffect的微任务,且此时没有提前存在的微任务,所以useEffect会先于Promise、setTimeout执行。
内容的提问来源于stack exchange,提问作者Rajneesh Mishra
相关产品推荐
相关产品推荐

