React.js学习-useContext性能优化

2022-05-19 21:41:412022-05-19 23:07:41

useContext在 React 中主要作为useState的替代品,用于在复杂组件间传递数据,但需要稍加注意一些组件重复渲染问题

先说结论:由于Context的限制,每当Context中的数据发生变化时,通过useContext使用该Context的组件及其子组件都会触发重渲。对此,可通过拆分Context,减少Context的作用域,减少重渲的范围

下面会以一个简单的计数器作为示例

一个简单的 🌰

在这里,我们创建了一个CounterContext,全局共享了state和一些action,页面中Header组件和Counter组件中都使用了CounterContext来取值,而且HeaderCounter分别有一个HeadererCounterer子组件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script
      src="https://cdn.jsdelivr.net/npm/react@18.1.0/umd/react.development.js"
      crossorigin
    ></script>
    <script
      src="https://cdn.jsdelivr.net/npm/react-dom@18.1.0/umd/react-dom.development.js"
      crossorigin
    ></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script type="text/babel">
      const {
        useContext,
        useReducer,
        createContext,
        useCallback,
        useState,
        memo,
        useMemo,
      } = React;

      const initialState = {
        count: 0,
      };

      const initialDispatch = {};

      const StateContext = createContext(initialState);

      const Store = ({ children }) => {
        const [state, setState] = useState({ count: 0 });

        const increment = useCallback(() => {
          setState((state) => ({
            ...state,
            count: state.count + 1,
          }));
        }, []);

        const decrement = useCallback(() => {
          setState((state) => ({
            ...state,
            count: state.count - 1,
          }));
        }, []);

        const value = useMemo(
          () => ({ state, decrement, increment }),
          [state, increment, decrement]
        );

        return (
          <StateContext.Provider value={{ state, increment, decrement }}>
            {children}
          </StateContext.Provider>
        );
      };

      const Header = () => {
        const { state } = useContext(StateContext);

        console.log("rerender Header");

        return (
          <div>
            <span>{state.count}</span>
            <Headerer />
          </div>
        );
      };

      const Counter = () => {
        const { increment, decrement } = useContext(StateContext);

        console.log("rerender Counter");

        return (
          <div className="counter">
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
            <Counterer />
          </div>
        );
      };

      const Headerer = () => {
        console.log("rerender Headerer");

        return <span>Headerer</span>;
      };

      const MHeaderer = memo(Headerer);

      const Counterer = () => {
        console.log("rerender Counterer");

        return <span>Counterer</span>;
      };

      const App = () => (
        <Store>
          <Header />
          <Counter />
        </Store>
      );

      const root = ReactDOM.createRoot(document.querySelector("#app"));
      root.render(<App />);
    </script>
  </body>
</html>

组件结构是这样的

组件结构

代码跑起来后页面显示正常,操作后数据也正常更新,但当我们打开控制台后,会发现打印的数据有点不正常

刷新页面,初次渲染时打印数据是正常的

rerender Header
rerender Headerer
rerender Counter
rerender Counterer

点击 ➕ 或 ➖ 后

rerender Header
rerender Headerer
rerender Counter
rerender Counterer
# <------点击按钮------->
rerender Header
rerender Headerer
rerender Counter
rerender Counterer

发现HeaderCounter及其子组件都触发更新了,好家伙,直呼好家伙

针对这个简单的 demo,我的Counter只是想用来渲染可更新数据的两个按钮,没必要重渲吧。我的HeadererCounterer也只是想展示两个 UI,为啥也更新了

原因开头已经表述过了,但具体的解决方(代)案(码)是什么呢

talk is cheap, show me the code

未使用到 Context 的

对于像HeadererCounterer这种没有直接使用到Context的,可通过寻常的解法:memo搞定

const MHeaderer = memo(Headerer);

const Header = () => {
  return (
    <div>
      <MHeaderer />
    </div>
  );
};

这样当再次重渲时就会跳过HeadererCounterer

使用到 Context 的

针对于像Counter这种直接使用到Context的,memo已经搞不定了,不过由于statesetState并不强制绑定,我们可通过将其拆分到两个Context来避免不必要的重渲,即:

分别创建两个Context,来将statesetState共享到全局

const StateContext = createContext(initialState);
const DispatchContext = createContext(initialDispatch);

const Store = ({ children }) => {
  const dispatch = useMemo(
    () => ({ decrement, increment }),
    [increment, decrement]
  );

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
};

在这里,DispatchContextvalue必须使用useMemo缓存一下:value是一个对象,触发state更新后Store组件会重渲,如果不缓存,value也会改变,导致使用到DispatchContext的组件也会重渲,拆分了个寂寞。。。因此拆分后一定要记得打印一下日志,看有没有效果

此时的组件结构是这样的

优化后的useContext

经过此番优化后,日志打印正常了

rerender Header
rerender Headerer
rerender Counter
rerender Counterer
# <------点击按钮------->
rerender Header

总结

  1. 对于未直接使用到Context的,可通过memo等手段优化,避免不必要的重渲

  2. 对于直接使用到Context的,可通过拆分Context优化