Skip to content

Instantly share code, notes, and snippets.

@zxhfighter
Last active June 10, 2020 01:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zxhfighter/72fbb8868dba847d410d9bb0a5f796dc to your computer and use it in GitHub Desktop.
Save zxhfighter/72fbb8868dba847d410d9bb0a5f796dc to your computer and use it in GitHub Desktop.
batch update react state

React 中的状态 batch 更新

状态更新

类组件

类组件可以使用 setState 更新状态。

函数组件

函数组件可以使用 const [value, setValue] = useState(0) 中的 setXXX 来更新状态。

并且这些设置状态的函数可能在一个函数或者类中出现很多次,那么 React 什么时候来决定重新渲染呢?

batch 逻辑

  1. 如果多个状态更新在同一个 React-based 事件中,例如按钮点击事件,输入框变更事件等,那么这些事件中的多个 setStatesetXXX 会批量缓存,直到该事件结束,才开始下一次渲染。例如下边代码中的 update() 点击中的所有状态变更会 batch,从而只有一次渲染。
function Batched() {
    const countRef = useRef(1);
    const [a, setA] = useState(0);
    const [b, setB] = useState(1);
    const [f, setF] = useState(2);

    function update() {
        // 由于在 React 同步事件处理范围内,所有同步的状态更新会 batch
        setA(v => v + 1);
        setB(v => v + 1);
        setF(v => v + 1);
    }

    useEffect(() => {
        countRef.current += 1;
    });

    return (
        <>
            <h1>渲染 {countRef.current}</h1>
            <p>a = {a}</p>
            <p>b = {b}</p>
            <p>f = {f}</p>
            <button onClick={update}>点击按钮触发一次渲染</button>
        </>
    );
}
  1. 如果状态更新不在 React-based 事件中,例如 setTimeoutsetIntervalasync 函数promises 等等,在这些函数中的状态更新不会批量缓存,一个状态变更触发一次组件新的渲染。
function NotBatched() {
    const countRef = useRef(1);
    const [a, setA] = useState(0);
    const [b, setB] = useState(1);
    const [f, setF] = useState(2);

    function update() {
        // 点击事件后,第二次渲染
        setA(v => v + 1);

        setTimeout(() => {
            // 由于不在 React 同步事件处理范围,这个函数里边所有状态更新不会缓存
            // setInterval,async 函数同理,参见下边的 asyncUpdate 函数

            // 这个会触发第三次渲染
            setB(v => v + 1);

            // 这个会触发第四次渲染
            setF(v => v + 1);
        }, 1000);
    }

    async function asyncUpdate() {
        // 点击后第 1 次渲染
        setA(v => v + 1);
        await waiting(1000);

        // 点击后第 2 次渲染
        setB(v => v + 1);

        // 点击后第 3 次渲染
       setF(v => v + 1);
    }

    useEffect(() => {
        countRef.current += 1;
    });

    return (
        <>
            <h1>渲染 {countRef.current}</h1>
            <p>a = {a}</p>
            <p>b = {b}</p>
            <p>f = {f}</p>
            <button onClick={update}>点击按钮触发三次渲染</button>
        </>
    );
}

手动 batch

上面所说的 React-based 事件中的状态更新会缓存,其实底层依赖的是 unstable_batchedUpdates 函数,这个函数会将事件处理函数包裹起来。

类似的,我们在定时器函数、Promises,async 函数中也可以手动调用该方法,达到批量缓存的目的。

import { unstable_batchedUpdates } from 'react-dom';

function ManualBatched() {
    const countRef = useRef(1);
    const [a, setA] = useState(0);
    const [b, setB] = useState(1);
    const [f, setF] = useState(2);

    function update() {
        // 点击事件后,第二次渲染
        setA(v => v + 1);

        setTimeout(() => {
            unstable_batchedUpdates(() => {
                // 这两个状态变更会 batch
                setB(v => v + 1);
                setF(v => v + 1);
            });

        }, 1000);
    }

    async function asyncUpdate() {
        // 点击后第 1 次渲染
        setA(v => v + 1);
        await waiting(1000);

        unstable_batchedUpdates(() => {
            // 这两个状态变更会 batch
            setB(v => v + 1);
            setF(v => v + 1);
        });
    }

    useEffect(() => {
        countRef.current += 1;
    });

    return (
        <>
            <h1>渲染 {countRef.current}</h1>
            <p>a = {a}</p>
            <p>b = {b}</p>
            <p>f = {f}</p>
            <button onClick={update}>点击按钮触发三次渲染</button>
        </>
    );
}

其余解决方案

逻辑分组

将一些逻辑相关的变量组合成一个对象,因为 useState 中可以是任何值,更新时使用扩展运算符。

setState(prevState => {
  return {...prevState, loading, data};
});

useReducer

一个例子如下:

import {useReducer} from 'react'
import {isEqual} from 'lodash';

const getData = url => {
  // setState 一般为派发函数 dispatch,这里将新的对象合并到已有对象(或者覆盖已有值)
  const [state, setState] = useReducer(
    (oldState, newState) => {
        const mergeState = {...oldState, ...newState};
        // 如果值没有变更,那么返回原始引用,避免依赖 state 的接口请求
        if (isEqual(oldState, mergeState)) {
            return oldState;
        }
        return mergeState;
    },
    {loading: true, data: null}
  )

  useEffect(async () => {
    const test = await api.get('/people')
    if(test.ok){
      setState({loading: false, data: test.data.results})
    }
  }, [])

  return state
}

参考资料

React currently will batch state updates if they're triggered from within a React-based event, like a button click or input change. It will not batch updates if they're triggered outside of a React event handler, like a setTimeout().

React wraps your event handlers in a call to unstable_batchedUpdates(), so that your handler runs inside a callback. Any state updates triggered inside that callback will be batched. Any state updates triggered outside that callback will not be batched. Timeouts, promises, and async functions will end up executing outside that callback, and therefore not be batched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment