【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 batch
      resetRenderTimer();
      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 来批量处理这些任务。

<h3>回答1:</h3><br/>React Hooks 的 useState 可以用来更新状态useState 返回一个数组,第一个元素是当前状态值,第二个元素是一个函数,用于更新状态值。可以通过调用这个函数并传入新的状态值来更新状态。例如: ``` import React, { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` 在这个例子中,我们使用 useState 创建了一个名为 count 的状态变量,并将其初始值设置为 0。我们还使用了 setCount 函数来更新 count 的值。当用户点击按钮时,我们会调用 setCount 并传入一个新的值,这个新的值会被用来更新 count 的值。 <h3>回答2:</h3><br/>React Hooks 中的 useState 是许多 React 开发者在创建组件时会经常用到的一个 Hook。它旨在帮助开发者在组件中存储和更新本地状态。 通过 useState,我们可以将组件中的状态数据添加到函数中,而不需要类组件的构造函数或 setState 方法。useState 的第一个返回值是当前状态的值,第二个返回值是更新状态值的方法。该方法可以用于更新状态并重新渲染组件。 当调用 useState 时,我们必须传入一个参数,它用于初始化状态的值。例如: ``` import React, { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` 在上述代码中,count 是一个状态,并且初始值为 0。setCount 是一个函数,它可以用于更新 count 的值。当点击按钮时,调用 setCount 来更新 count 的值,这样组件会重新渲染并显示新的 count 值。 useState 的一个重要的使用场景是在处理表单数据时。通常我们需要在表单中存储用户输入的数据并在表单提交时将其发送到服务器。我们可以使用 useState 来存储表单数据并更新它们。 总结起来,useStateReact Hooks 中用于在组件中存储和更新状态的重要Hook之一,可以帮助开发者在构建React组件时更加便捷,实现清晰易读的代码。 <h3>回答3:</h3><br/>React Hooks 已经成为 React 中一个非常重要的功能。在使用 Hooks 时,最常用的是 useState 函数。这个函数提供了一种方便且简单地方式来在 Functional Components 中定义状态,并且可以使用 setState 函数来更新状态useState 的语法非常简单,它接受一个参数,表示状态的初值。然后,它返回一个数组,该数组包含两个值。第一个值是当前的状态,第二个值是一个函数,用于更新状态。我们可以把这个函数叫做 setState 函数。 在使用 setState 函数时,我们首先需要理解的是它是一个异步函数。这意味着,当我们调用 setState 函数时,React 并不会立即更新状态。相反,它会先对比新值和旧值,然后将新的状态合并到原来的状态对象中,最后在下一次 render 时,将新的状态值更新到组件中。 如果我们需要在更新状态后做一些操作,例如向服务器发送请求或者更新页面元素,我们需要使用 useEffect Hooks。 具体来说,useState 函数接收的参数是状态的初始值,可以是任意类型的值,而更新状态时可以调用 setState 函数,它接收一个新的状态值作为参数。setState 函数可以是一个函数,用于更新状态值,该函数接收一个参数 prevState,表示当前状态值。 例如,我们可以这样写一个计数器组件: ``` function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } ``` 在上面的例子中,我们使用了 useState 来定义了一个 count 的状态,初值为 0。然后,我们在组件的 render 方法中使用了这个状态,并且实现了一个增加按钮的逻辑。点击按钮时,我们通过调用 setCount 函数来更新状态。 总之,useState 是建立和管理状态的函数,它会返回一个数组,其中两项分别代表当前状态和更新状态的函数。使用它来更新状态是非常方便的,同时也充分体现了 React Hooks 在状态管理方面更加简单、便捷的特点。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值