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

React中useEffect与useLayoutEffect实现原理及相关问题咨询

useEffect与useLayoutEffect底层实现逻辑

我们可以结合React的渲染流水线来拆解两个钩子的执行逻辑:
React的一次更新分为两个核心阶段:

  1. Render阶段:遍历组件树、计算状态变化、生成更新后的Fiber节点树
  2. Commit阶段:将计算好的DOM变化实际更新到真实DOM上,这个阶段又分为三个同步执行的子阶段:before mutation → mutation → layout

useLayoutEffect实现逻辑

  • 注册逻辑:在Render阶段执行组件函数时,useLayoutEffect的回调会被存入当前Fiber节点的更新队列中,标记为高优先级同步任务。
  • 执行逻辑:在Commit阶段的layout子阶段同步执行所有useLayoutEffect回调,整个过程是同步阻塞的,不会让位给浏览器的事件循环。
    这也刚好解释了你观察到的Synchronous 5: Ran after useLayoutEffect的结果:ReactDOM.render是同步调用,调用后会同步走完Render+Commit全流程,执行完所有useLayoutEffect回调之后才会返回,执行后面的Log(5)同步代码,自然比你之前调度的微任务执行时机更早,它完全不依赖浏览器的微/宏任务调度,走的是React内部同步任务队列。

useEffect实现逻辑

  • 注册逻辑:同样在Render阶段存入Fiber节点的更新队列,但会被标记为低优先级异步任务。
  • 执行逻辑:Commit阶段的layout子阶段结束后,React会先判断是否有需要执行的useEffect队列,随后通过scheduler调度库向浏览器事件循环提交一个低优先级宏任务(现代浏览器优先用MessageChannel实现,降级方案为setTimeout),等当前同步任务、已入队的微任务、同优先级的已入队宏任务都执行完之后,再统一执行所有useEffect回调。
    你观察到的Task 4: Ran after useLayoutEffect、Task 5: Ran after useEffect完全符合这个逻辑:你在Log里调用的setImmediate和React调度useEffect的宏任务优先级持平,Log(4)的setImmediate入队时间早于React调度useEffect的时间,所以Task4先执行,之后才会执行useEffect回调修改ref的值,最后输出Task5。
    至于你观察到的“处理effect的任务似乎预先调度”,是因为React在Render阶段就已经统计好了所有需要执行的effect列表,commit阶段结束后直接触发调度即可,不需要等钩子调用时再临时处理。

附加问题解答

严格模式在开发环境下的重复渲染逻辑,专门做了控制台日志的去重处理:
第一次执行组件函数时,所有同步的console.log输出会被React内部静默屏蔽,只保留第二次组件执行的同步日志;但你通过queueMicrotask、setImmediate调度的异步任务的日志不属于同步执行上下文,不会被React屏蔽,所以就出现了异步日志重复、同步日志只有一次的现象。
你可以把React DevTools设置里的「Hide logs during second render in Strict Mode」开关关掉,就能看到两次重复的同步日志。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 10:57:03