likes
comments
collection
share

React性能优化之batchedUpdates

作者站长头像
站长
· 阅读数 37

前言

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 (
    <div>
      render count {renderCount.current}
      <div>first value: {first}</div>
      <div>second value: {second}</div>
      <p>
        <button onClick={syncClickHandle}>sync setState</button>
        <button onClick={asyncClickHandle}>async setState</button>
        <button onClick={onClearHandle}>clear</button>
      </p>
    </div>
  );
}

demo运行效果:

React性能优化之batchedUpdates

BatchUpdateComponent组件内部有两个state:firstsecond,点击button后会同时触发setState,syncClickHandle 采用同步方法更新两个state,asyncClickHandle采用异步方式更新两个state(setState动作在Promise.resolve()方法体里)。

为了记录组件的re-render次数,定义了一个useRef钩子 renderCount,在renderCount在每次更新结束后自增1。

sync setState

首先,点击sync setState button,看下同时更新两个state时会触发几次re-render:

React性能优化之batchedUpdates

可以发现re-render的值等于两个state的值,证明虽然触发了两次state更新,但是只执行了一次re-render,批量更新机制体现地淋漓尽致。

async setState

再看下点击 async setState button后的表现:

React性能优化之batchedUpdates

上图是点击了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>
      <p>
        <button onClick={syncClickHandle}>sync setState</button>
        <button onClick={asyncClickHandle}>async setState</button>
        <button onClick={asyncWithBatchClickHandle}>
          async batchedUpdates setState
        </button>
        <button onClick={onClearHandle}>clear</button>
      </p>
    </div>
  );
}

demo效果:

React性能优化之batchedUpdates

增加了一个 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后的效果:

React性能优化之batchedUpdates

发现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性能优化之batchedUpdates

总结

  • React 16, 17在处理异步事件的场景中,比如: async/awaitthen/catchsetTimeoutfetch。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 浅析

转载自:https://juejin.cn/post/7125628743296483342
评论
请登录