You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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. 打印1
    2. 执行50ms的while循环,阻塞主线程
    3. 注册useEffect回调(仅注册,此时不执行)
    4. Promise.resolve().then()将打印3的回调加入微任务队列
    5. setTimeout(() => console.log(4), 0)将打印4的回调加入宏任务队列(由于主线程被阻塞50ms,该回调的实际执行时机在阻塞结束后)
  • React commit阶段:同步完成组件DOM挂载
  • 事件循环处理:
    1. 主线程空闲后,优先处理当前微任务队列:执行Promise回调,打印3
    2. 微任务队列清空后,处理宏任务队列:执行setTimeout回调,打印4
    3. 本次宏任务执行完毕后,React调度执行useEffect的回调(挂载阶段的effect),打印2

这就解释了初始渲染的顺序:1 → 3 → 4 → 2

2. 点击按钮后的更新渲染

执行流程:

  • 点击事件同步代码:
    1. 打印5
    2. 调用setState,React标记组件需要更新(18版本中默认批量更新,不会立即触发渲染)
    3. 打印6
  • React更新渲染:
    1. 同步重新执行App组件,打印1
    2. 注册新的useEffect回调,同时检测到依赖state变化,标记需要执行更新后的effect
    3. 注册Promise微任务(打印3)和setTimeout宏任务(打印4)
  • React commit阶段:同步完成DOM更新
  • 事件循环处理:
    1. React在commit阶段结束后,将useEffect的回调优先加入微任务队列
    2. 处理微任务队列:先执行useEffect回调打印2,再执行Promise回调打印3
    3. 微任务队列清空后,处理宏任务队列:执行setTimeout回调打印4

这就解释了更新渲染的顺序:5 → 6 → 1 → 2 → 3 → 4

为何两次渲染顺序不同?

核心原因是初始渲染与更新渲染的调度时机差异:

  • 初始渲染时,组件同步代码中先添加了Promise微任务,React在commit阶段后添加的useEffect微任务会排在已有微任务之后;同时由于50ms阻塞,setTimeout的宏任务已经到达执行时机,在微任务清空后立即执行,导致useEffect回调最后执行。
  • 更新渲染时,React在commit阶段后会优先调度useEffect的微任务,且此时没有提前存在的微任务,所以useEffect会先于Promise、setTimeout执行。

内容的提问来源于stack exchange,提问作者Rajneesh Mishra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 06:36:12