React16.8引入了
Hook
,赋予了
Function Component
使用 state 以及其他的 React 特性的能力。因为
Function Component
写法的灵活性,越来越多的开发者开始使用
Function Component
的写法。
Function Component
带来灵活性的同时也带来一些性能损耗,因为在
Function Component
中,没有一个上下文对象去保存内部状态信息,每一次组件更新都意味着整个函数上下文的重新执行,所有变量、常量都重新声明,执行完毕,函数内部的变量再被垃圾机制回收。所以,为了减少因
setState
带来的重复渲染,React在useState hook的实现中采用了
批量更新
策略。
关于批量更新的原理,本文不再赘述,本文重点放在批量更新的适用场景,已经React默认的批量更新失效后如何去优化。
批量更新问题
看下面一个demo
下面demo的 React 版本为
17.0.2
import React, { useState, useRef, useEffect } from "react";
export default function BatchUpdateComponent() {
const [first, setFirst] = useState(0);
const [second, setSecond] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
const syncClickHandle = () => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
const asyncClickHandle = () => {
Promise.resolve().then(() => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
const onClearHandle = () => {
setFirst(0);
setSecond(0);
renderCount.current = 0;
return (
render count {renderCount.current}
<div>first value: {first}</div>
<div>second value: {second}</div>
<button onClick={syncClickHandle}>sync setState</button>
<button onClick={asyncClickHandle}>async setState</button>
<button onClick={onClearHandle}>clear</button>
</div>
demo运行效果:
BatchUpdateComponent
组件内部有两个state:first
、second
,点击button后会同时触发setState,syncClickHandle 采用同步方法更新两个state,asyncClickHandle采用异步方式更新两个state(setState动作在Promise.resolve()
方法体里)。
为了记录组件的re-render次数,定义了一个useRef
钩子 renderCount
,在renderCount
在每次更新结束后自增1。
sync setState
首先,点击sync setState button,看下同时更新两个state时会触发几次re-render:
可以发现re-render的值等于两个state的值,证明虽然触发了两次state更新,但是只执行了一次re-render,批量更新机制体现地淋漓尽致。
async setState
再看下点击 async setState button后的表现:
上图是点击了8次的效果,两个state的值更新到了8,然而re-render次数却来到了16,state的批量更新没有生效!
想要在异步任务中也实现state的批量更新以减少re-render损耗有两种办法:
使用react-dom
库的unstable_batchedUpdates
方法强制批量更新
升级到 React18
1.unstable_batchedUpdates
以 unstable_
开头的 API。 当一些功能还不够稳定时,这些 API 会作为试验性功能提供。通过以 unstable_
为前缀的方式发布这些 API,我们能够更快地迭代,更早地推出稳定的功能。
正如官网所说的,batchUpdates API是带有试验性的API,其贴上试验性标签不是因为API本身不稳定,而是React团队期望在未来某一天将批量更新自动化的方式应用到更多合适的场景中。
当然,随着React18的推出,可以确信React团队确实实现了这点。
好了,既然有了强制批量更新API,那剩下的就是使用它了,上代码:
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./index.css";
export default function IndexComponent() {
const [first, setFirst] = useState(0);
const [second, setSecond] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
const syncClickHandle = () => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
const asyncClickHandle = () => {
Promise.resolve().then(() => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
const asyncWithBatchClickHandle = () => {
Promise.resolve().then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
const onClearHandle = () => {
setFirst(0);
setSecond(0);
renderCount.current = 0;
return (
<div className="index">
render count {renderCount.current}
<div>first value: {first}</div>
<div>second value: {second}</div>
<button onClick={syncClickHandle}>sync setState</button>
<button onClick={asyncClickHandle}>async setState</button>
<button onClick={asyncWithBatchClickHandle}>
async batchedUpdates setState
</button>
<button onClick={onClearHandle}>clear</button>
</div>
demo效果:
增加了一个 async batchedUpdates setState
button,在其回调函数中执行更新两个state的操作,不同的是更新state动作放到了 ReactDOM.unstable_batchedUpdates
方法里:
const asyncWithBatchClickHandle = () => {
Promise.resolve().then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setFirst((prev) => prev + 1);
setSecond((prev) => prev + 1);
看一下点击button后的效果:
发现re-render的次数跟state的值“同步”了,证明实现了state批量更新!
2.升级到React18
这种方式简单粗暴,看下async setState button点击后在 React18 下的效果(代码完全一样,只是React版本升级到了18.2.0)
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
关于React18 实现原子化的批量更新说明可以参看这里
可以发现异步方法体的state更新以批量更新的方式进行:
React 16, 17在处理异步事件的场景中,比如: async/await
, then/catch
, setTimeout
, fetch
。state的更新不会以批量的方式进行。
React 16, 17中异步时间回调里更新state时可以使用 ReactDOM.unstable_batchedUpdates
API强制批量更新。
React 18后,原子化的state批量更新已经可以覆盖异步回调场景。
React State Batch Update
Automatic batching for fewer renders in React 18
ReactDOM.js
深入 react 细节之 - batchUpdate
Batch Update 浅析