React 更新与重新渲染
senmu2024/03/31
前言
众所周知,React 存在 Class 组件和 Function 组件,从官方给的 API 来看 Class 组件的更新使用 this.setState()
、this.forceUpdate()
,Function 组件使用 Hook 的形势来实现更新比如:useState
、useReducer
。
接下来就从 Class 组件开始看起。
this.setState()
的更新
更新流程
- 调用 Component 上的
SetState
方法 - 调用 updater 上的
enqueueSetState
方法,如下图: - 设置好更新队列后调度执行
如何处理多次更新的情况?
- 将所有同步的更新加入队列
- 在 render 阶段前取出更新队列中的任务,将它们串成链表
- 进入 render 阶段就是处理更新队列逻辑(
processUpdateQueue(workInProgress, newProps, instance, renderLanes)
)
- 如果
setState
中传的是回调函数的话就会将回调函数推进回调队列 - 如果
setState
中传的是对象的话,就只会取最后一个setState
传递的对象
疑问🤔
- 为什么同步调用多次
this.setState
会对他们进行批处理?- 这是因为 React 对于事件、生命周期函数等都使用了
batchUpdates
包裹,通过该方法包裹的函数会将变量executionContext
设置为 1(默认为 0) - 那将 `` 设置为 1 与 0 有什么区别呢?
- 区别很大,请看下面代码
// NoContext 默认为 0,如果 executionContext 默认为 0 的话就会进入该逻辑 if (lane === SyncLane && executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode && !( ReactCurrentActQueue$2.isBatchingLegacy)) { resetRenderTimer(); flushSyncCallbacksOnlyInLegacyMode(); // 会立刻执行调度任务 }
- 以上的代码就解释了为什么未被
batchUpdates
包裹的异步函数会同步执行setState
- 等等,你还是无法理解?
- 那么还有一点就是事件循环的逻辑,因为 js 遇到异步任务会将该任务交给浏览器然后加入任务队列,等待被调度(也就是说需要等待 React 同步逻辑调用完毕才会执行下一次的异步任务)
- 这是因为 React 对于事件、生命周期函数等都使用了
setState
的更新
hook 中 hook 中的状态更新与上面 this.setState()
类似,都是
更新流程
- 注册
useState
hook,返回dispatchSetState
更新函数
- 期间会注册好更新队列并保存状态
- 调用
dispatchSetState
函数
- 需要注意的是,这里会针对状态一样的更新做复用处理(
eagerState
);当然,只有节点的优先级未被设置且不存在alternate
才会进行该处理。
- 加入并发更新队列后调度更新
如何处理多次更新的情况?
和 class 组件的处理类似,不过值得注意的是由于闭包的存在,没有经过批量处理包裹的更新最后得出来的结果有差异,举个例子:
/**
* class 组件
* 如果不经过 batchUpdates 包裹,最终得到的结果是 3(假设原 count 为 0)
*/
this.setState({
count: this.state.count+2
})
this.setState({
count: this.state.count+1
})
/**
* function 组件
* 如果不经过 batchUpdates 包裹(函数式组件 useEffect 是通过 flushPassiveEffectsImpl 设置的 executionContext),最终得到的结果是 1(假设原 count 为 0)
* 这里搞清楚批量更新本质就是通过设置 executionContext 不为 0 就可以了
*/
setCount(count+2)
setCount(count+3)
setCount(count+1)
// 为什么会有这样的差异呢? 这是因为函数式组件的 count 取的是闭包中 count 的值
异步调用多次更新又会怎样?
从上面的例子中我们得知有闭包的存在,所以异步调用与同步调用没啥区别