从源码角度看 React 的批量更新

背景

最近对 react 的合并更新比较感兴趣,所以就研究了一下(整整 2 天!!!)

前言

这次主要是从调用栈中来进行分析,好,废话不多说,直接开始

案例

下面这个案例在 react17 和 react18 有不同的表现

jsx 复制代码
import { useState } from 'react'

const App = () => {
  const [count, setCount] = useState(0)

  console.log('触发更新', count)

  const update111111111 = (c) => {
    console.log('1111');
    return c + 1
  }

  const update222222222 = (c) => {
    console.log('2222');
    return c + 1
  }

  const update333333333  = (c) => {
    console.log('33333');
    return c + 1
  }

  const handleClick = () => {
    // 同步setState
    setCount(update111111111)
    setCount(update222222222)
    setCount(update333333333)

    // 异步setState
    setTimeout(() => {
      setCount(count => {
        return count + 3
      })
      setCount(count => {
        return count + 3
      })
    })
  }
  
  return (
    <button className="btn" onClick={handleClick}>
      <span role="img" aria-label="react-emoji">⚛️</span> {count}
    </button>
  )
}


export default App

React 17 表现

在点击按钮后,可以看到页面的输出如下:

也就是这里触发了 3 次组件更新。

其中同步的 3 次 setState 被合并成了 1 次更新,异步的 2 次 setState 触发了 2 次组件更新

同步 setState 解析

给 更新 console 打个断点:

点击按钮后来看调用栈:

上面有红字具体解析。因为 handleClick 中的 3 次 setState 执行完之后,executionContext 才被还原,在到达 finally 的时候,才会触发组件更新,所以这里 3 次 setState 只触发了一次组件更新

异步 setState 更新

给 更新 console 打个断点:

点击按钮,跳过一次断点,然后进入到 setTimeout 中 setState 导致的更新

在执行 handleClick 中的 setTimout 任务时,discreteUpdates$1 函数已经执行完毕了,也就是此时的 executionContext 就是 NoContext。

setTimeout 中的更新在 scheduleUpdateOnFiber 函数中会被执行(所以在 React 18 之前,setTimeout 中的 state 都是同步更新的)

又由于 setTimeout 中执行了 2 次 setState,所以这里触发了 2 次组件更新

React 18 表现

这里的应用初始化方式用的 createRoot,在点击按钮后,可以看到页面的输出如下:

也就是这里只触发了 2 次组件更新,同步的 3 次 setState 被合并成了 1 次更新,异步的 2 次 setState 被合并成了 1 次更新

同步 setState 解析

首先来看点击按钮时的调用栈:

这里给 第一个 setCount 打一个断点

点击按钮,进入断点,断点进入 setState 内部查看,查看调用栈

图上有红字解析(某些红字不理解没关系,下面更新过程中会有解析)

这里点击函数执行前,对 executionContext 并入了 BatchedContext,代表属于此时属于批量更新上下文。然后在 finally 的时候对 executionContext 进行了还原。

重点来了:我们知道在 handleClick 中,有 3 次 setState 都是同步的,所以在我们执行完 3 次 setState 后,这个 executionContext 才会进行还原

然后给 console 打个断点:

点击按钮,进行查看更新的调用栈:

这里组件更新的任务为微任务,scheduleMicrotask 为微任务调度,其中 flushSyncCallbacks为组件更新函数入口。然后因为上面的 executionContext 的还原操作是属于同步的,所以在微任务执行后,executionContext 是为 NoContext 的,所以可以正常执行更新。

这就完成了一次批量更新,针对 3 次 setState 的结果,合并成了 1 次更新

异步 setState 更新

对 console 打断点:

然后点击按钮,跳过一次断点,来到 setTimeout 的更新

注:这里的 postMessage 也是宏任务的一种

这里可以看到 setTimeout 的执行,在 flushWork(组件更新入口函数) 之前。那就意味着,setTimeout 中的 2 次 setState 计算完毕后,才会到 flushWork 执行,所以这就完成了 setTimeout 中的 2 次 setState 合并,只会触发一次更新

最后

看了网上比较多的帖子,感觉都讲的不是很清楚,所以还是决定自行研究一番,收获还是满满的

相关推荐
摸鱼仙人~11 分钟前
Redux Toolkit 快速入门指南:createSlice、configureStore、useSelector、useDispatch 全面解析
开发语言·javascript·ecmascript
小声读源码35 分钟前
【技巧】dify前端源代码修改第一弹-增加tab页
前端·pnpm·next.js·dify
假客套1 小时前
2025 后端自学UNIAPP【项目实战:旅游项目】7、景点详情页面【完结】
前端·uni-app·旅游
程序员小张丶1 小时前
基于React Native开发HarmonyOS 5.0主题应用技术方案
javascript·react native·react.js·主题·harmonyos5.0
Captaincc1 小时前
Ilya 现身多大毕业演讲:AI 会完成我们能做的一切
前端·ai编程
teeeeeeemo1 小时前
Vue数据响应式原理解析
前端·javascript·vue.js·笔记·前端框架·vue
Sahas10191 小时前
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not explicitly defined.
前端·javascript·vue.js
Jinxiansen02111 小时前
Vue 3 实战:【加强版】公司通知推送(WebSocket + token 校验 + 心跳机制)
前端·javascript·vue.js·websocket·typescript
MrSkye1 小时前
React入门:组件化思想?数据驱动?
前端·react.js·面试
BillKu2 小时前
Java解析前端传来的Unix时间戳
java·前端·unix