您的位置:首页 > 财经 > 产业 > 【React Hooks - useState状态批量更新原理】

【React Hooks - useState状态批量更新原理】

2024/11/17 12:12:07 来源:https://blog.csdn.net/weixin_44273311/article/details/141321381  浏览:    关键词:【React Hooks - useState状态批量更新原理】

概述

所谓批量处理就是当在同时更新多个状态下,能够统一批量处理更新,避免了重复渲染。在React17及之前版本,React只会在合成事件以及生命周期内部进行批量处理,在setTimeout、Promise、Fetch等异步请求中,则不会自动批量处理,需要使用unstable_batchedUpdatesAPI手动处理。而在React18对其进行了优化,不管什么条件下,默认都会批量处理。本文主要就是从demo实例结合bugger源码的方式来解释在React17和18中对于状态批量更新的逻辑介绍。

React17

从概述可知,React17版本,默认只会在合成事件、生命周期内批量处理,在异步请求中需要手动处理,先看下面demo代码:

import React, { Fragment, useState } from 'react';export default function Component() {const [a, setA] = useState(1);console.log('a', a);// 异步请求,不会自动批量,会渲染多次,该示例中会render4次function handleClickWithPromise() {Promise.resolve().then(() => {setA((a) => a + 1);setA((a) => a + 1);setA((a) => a + 1);setA((a) => a + 1);});}// 绑定点击事件,会自动批量,只会render一次function handleClickWithoutPromise() {setA((a) => a + 1);setA((a) => a + 1);setA((a) => a + 1);setA((a) => a + 1);}return (<Fragment><button onClick={handleClickWithPromise}>{a} 异步执行</button><button onClick={handleClickWithoutPromise}>{a} 同步执行</button></Fragment>);
}

先解释一下上面说的合成事件:React 中的合成事件是 React 自己实现的一套跨浏览器兼容的事件处理机制。它将浏览器原生事件封装为统一的 API,以确保在不同浏览器中的行为一致。通过事件委托和事件池化,React 可以更高效地管理事件监听器,并减少内存开销。合成事件使得开发者能够以一致的方式处理各种用户交互事件,无需关心浏览器之间的差异。即如下图所示:
在这里插入图片描述
上面的demo在浏览器中(Chrome为例),点击同步按钮会打印5,点击异步按钮则会打印2,3,4,5render4次。

因为React会自动合并,所以只能通过setA((a) => a + 1)或者setA(2)这种具体值的方式才会符合上述,由于React会自动尝试合并操作,如果书写为setA(a + 1)则只会打印2次(由于 React 在异步上下文中处理状态更新时的行为,React 17 可能会导致组件重新渲染两次)

接下来我们在浏览器开启debugger来了解其内部逻辑。

同步执行

同步执行下,会自动批量处理,只会render一次。
在bugger下我们点击同步按钮能看到整个函数执行的调用栈,其中主要看标记的几个函数:
在这里插入图片描述
其中顶层函数就是我们点击同步按钮执行的回调,然后继续单步调整会发现其进入了React的dispatchAction来创建一个状态更新任务,然后会调用scheduleUpdateOnFiber进入Scheduler调度器中等待执行,这个阶段本文不再介绍,有兴趣的可以查看这篇文章:【React Hooks原理 - useState】

直到最后一个状态更新执行完成,会根据当前调用栈往上回调,然后来到标记的第二个函数batchedEventUpdates$1

function batchedEventUpdates$1(fn, a) {var prevExecutionContext = executionContext;executionContext |= EventContext;try {return fn(a);} finally {executionContext = prevExecutionContext;if (executionContext === NoContext) {// Flush the immediate callbacks that were scheduled during this batchresetRenderTimer();flushSyncCallbackQueue();}}
}

当我们在页面点击同步按钮时,就会触发React的合成事件,进而进入上面的函数中,在其中主要做以下事:

  • 保存当前上下文,并更新当前上下文为EventContext,默认是NoContext
  • 执行传入的回调函数
  • 回退当前上下文为自身的上下文,并判断是否执行更新

由于是同步执行,所以当状态更新回调执行完后,会进入上面的代码中的finally,此时所有的状态更新都保存在更新队列中的,然后执行flushSyncCallbackQueue回调,进行批量更新,所以只会render一次。

异步执行

在setTimeout、Promise、Fetch等异步回调中,不会自动批量处理,需要手动使用unstable_batchedUpdates。如上述所说,在异步条件下,上面的demo会render4次。
在这里插入图片描述
从图中能看出,点击按钮时也会经过batchedEventUpdates函数的封装,并在其设置上下文进去批量更新(同步逻辑),但是在异步情况下,异步回调会在当前任务执行完成之后在执行,执行时已经脱离的设置的批量上下文,所以当进入finally中批量更新时,此时更新队列并没有当前新的更新任务,等到更新任务执行时,此时上下文已经不再是批量上下文,所以会依次执行状态更新而导致重复render。

unstable_batchedUpdates

使用该API可以强制将其中的回调同步执行,可以用于在异步请求中批量处理。其本质就是batchedUpdates$1函数,所以当在异步请求中将状态更新放在其内部,会批量处理。
在这里插入图片描述
通过bugger也能发现,其实际还是执行的batchedUpdates$1函数,逻辑和同步一致,通过设置上下文然后调用flushSyncCallbackQueue()批量处理更新任务,区别就是由于其仍然处于异步回调用,所以执行时机仍然会延迟,等待同步代码执行完成之后执行。
在这里插入图片描述

总结

在点击按钮触发状态更新时,实际触发的是经过batchedUpdates$1处理的合成事件。同步代码中在状态更新时将更新任务添加到队列中(此时上下文已经更新为批量上下文),最后在finally中执行flushSyncCallbackQueue批量更新状态。而在异步回调中会脱离批量上下文,通过使用unstable_batchedUpdates包裹,收到执行batchedUpdates$1函数,在执行时重新设置批量上下文,并调用flushSyncCallbackQueue批量更新,本质还是通过batchedUpdates$1函数执行批量,无非一个是自动一个是手动的区别。

React18

在React18之后,主要新增了并发特性和对批量更新进行了优化,不管在异步还是同步回调中都默认进行批量处理。下面同样使用上面的demo代码,在Chrome下不管点击同步还是异步按钮都只会render一次。下面在React18环境下进行bugger流程介绍。

同步执行

在浏览器调试我们知道,在React17中当对状态进行更新的时会通过dispatchAction调用scheduleUpdateOnFiber等待调度更新,而在18中对其进行了优化,不会直接调度更新,而是在dispatchSetState中通过enqueueConcurrentHookUpdate将状态更新添加到等待执行的队列中,待执行完成之后再统一批量更新。
在这里插入图片描述
同React17,在18中状态更新回调执行完成之后,会回到batchedUpdates$1函数,此时所有的更新任务都以链表的方式保存在队列中。

function batchedUpdates$1(fn, a) {var prevExecutionContext = executionContext;executionContext |= BatchedContext;try {return fn(a);} finally {executionContext = prevExecutionContext; // If there were legacy sync updates, flush them at the end of the outer// most batchedUpdates-like method.if (executionContext === NoContext && // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.!( ReactCurrentActQueue$1.isBatchingLegacy)) {resetRenderTimer();flushSyncCallbacksOnlyInLegacyMode();}}
}

如上面所说,该函数主要就是设置批量上下文,执行传入的更新回调,然后在finally中通过flushSyncCallbacksOnlyInLegacyMode函数然后执行flushSyncCallbacks同步更新状态。

function flushSyncCallbacksOnlyInLegacyMode() {// Only flushes the queue if there's a legacy sync callback scheduled.// TODO: There's only a single type of callback: performSyncOnWorkOnRoot. So// it might make more sense for the queue to be a list of roots instead of a// list of generic callbacks. Then we can have two: one for legacy roots, one// for concurrent roots. And this method would only flush the legacy ones.if (includesLegacySyncCallbacks) {flushSyncCallbacks();}
}

异步执行

React18对其优化之后,在异步请求中默认也会自动批量处理。和同步时一样也会通过enqueueConcurrentHookUpdate将更新任务添加到更新队列中,并不会直接调度更新。当执行到最后一个状态更新的setState时,会进入ensureRootIsScheduled的这块逻辑进入微任务的处理(其他setState会在上面就return,不会进入该逻辑):

scheduleMicrotask(function () {// In Safari, appending an iframe forces microtasks to run.// https://github.com/facebook/react/issues/22459// We don't support running callbacks in the middle of render// or commit so we need to check against that.if ((executionContext & (RenderContext | CommitContext)) === NoContext) {// Note that this would still prematurely flush the callbacks// if this happens outside render or commit phase (e.g. in an event).flushSyncCallbacks();}});

在其中会执行flushSyncCallbacks函数统一处理更新队列中的任务,最后只渲染一次。所以在React18中不管是同步还是异步,都是先将更新任务保存在待执行队列中,最后都是通过flushSyncCallbacks来批量处理状态更新的。

总结

同步更新: 状态更新任务入队并在 flushSyncCallbacks 中被批量处理。
异步更新: 状态更新任务同样入队,但在异步任务完成后,通过微任务调度机制调用 flushSyncCallbacks 来批量处理这些任务。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com