React.memo 使用的 4 种误区

react 中组件缓存的两种方式:

  1. 使用 memo 缓存子组件

    js 复制代码
    const App = () => {
      const [count, setCount] = useState(0);
      const onClick = () => setCount(count + 1);
      return (
        <>
          <div>
            {count}
            <button onClick={onClick}>按钮</button>
          </div>
          <Child />
        </>
      );
    };
    const Child = memo(() => {
      console.log("child render");
      return <div>Child</div>;
    });
  2. 使用 useMemo 缓存组件

    js 复制代码
    const App = () => {
      const [count, setCount] = useState(0);
      const onClick = () => setCount(count + 1);
      const ChildMemo = useMemo(() => <Child />, []);
      return (
        <>
          <div>
            {count}
            <button onClick={onClick}>按钮</button>
          </div>
          {ChildMemo}
        </>
      );
    };
    const Child = () => {
      console.log("child render");
      return <div>Child</div>;
    };

这两种方式的区别是:

  • memo 是通过比较 prevPropsnextProps 是否相同来决定是否更新
  • useMemo 是通过依赖是否变化来决定是否更新

什么时候使用 useMemo 缓存组件,什么时候使用 memo 缓存组件呢?

大部分情况都是使用 memo 取缓存组件的,因为 memo 的使用更简单,而且 memo 也是 react 官方推荐的

使用 useMemo 缓存的组件的话,可以用于切换组件的封装,比如下面的代码:

js 复制代码
{
  count % 2 === 0 ? <Child1 /> : <Child2 />;
}
🔽
const element = useMemo(() => (count % 2 === 0 ? <Child1 /> : <Child2 />), [count]);

在使用 memo 缓存组件时有些坑要注意,比如下面这几种

缓存失效1------未使用 useCallback

子组件使用了 memo,但是父组件没有使用 useCallback 包裹 onClick 函数,这样 memo 就失效了

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = () => console.log("app click");
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div>Child</div>;
});

因为在父组件中,每次 setCount,都会创建一个新的 onClick 函数,而 memo 是通过比较 prevPropsnextProps 来决定是否更新的,所以每次父组件更新,子组件也会更新

正确的写法是,在父组件中将 onClick 使用 useCallback 缓存起来

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div>Child</div>;
});

useCallback 错误使用

这种方式不会造成缓存失效,而是拿不到最新的状态

子组件中使用了 useCallback 缓存函数,但是依赖是空数组(或者有依赖忘记写了)

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  // count 永远是初始值 0
  const onClick = useCallback(() => console.log("app click", count), [count]);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  const [childCount, setChildCount] = useState(0);
  const onClickChildButton = useCallback(() => {
    setChildCount((childCount) => childCount + 1);
    onClick();
  }, []);
  return (
    <div>
      Child---{childCount}
      <button onClick={onClickChildButton}>child 按钮</button>
    </div>
  );
});

子组件中的依赖是空数组,它无法感知父组件 count 的变化

正确的做法是将将 onClick 放入 useCallback 的依赖项里

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  // count 永远是初始值 0
  const onClick = useCallback(() => console.log("app click", count), [count]);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  const [childCount, setChildCount] = useState(0);
  const onClickChildButton = useCallback(() => {
    setChildCount((childCount) => childCount + 1);
    onClick();
  }, [onClick]);
  return (
    <div>
      Child---{childCount}
      <button onClick={onClickChildButton}>child 按钮</button>
    </div>
  );
});

缓存失效------props 使用普通对象

将对象直接写在 jsx 的属性中,这种和上面的 useCallback 错误使用 1 是一样的,每次父组件更新,都会创建一个新的 style 对象,导致子组件更新

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={{ color: "red" }} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div style={style}>Child</div>;
});

正确的写法使用使用 useMemostyle 缓存起来

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  const style = useMemo(() => ({ color: "red" }), []); // 这里使用了 useMemo
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={style} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div style={style}>Child</div>;
});

当然也可以使用 useState 定义对象

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  const [style] = useState({ color: "red" }); // 这里使用 useState
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={style} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div style={style}>Child</div>;
});

缓存失效3------第三方状态管理工具

使用第三方状态管理工具,比如 zustand,在使用 memo 缓存组件时也会遇到失效的情况

如下代码:

  1. styleParent 是父组件使用的样式,styleChild 是子组件使用的样式
  2. 父组件中点击按钮,会更新 styleParent,同时子组件也会重新渲染,导致 memo 失效了
js 复制代码
const useStyle = create((set) => ({
  styleChild: { color: "red" },
  styleParent: { color: "blue" },
  setStyle: (color) => set({ styleParent: { color } }),
}));
function App() {
  const [count, setCount] = useState(0);
  const { setStyle, styleParent } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyle(styleParent.color === "blue" ? "green" : "blue");
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} />
    </div>
  );
}
const Child = memo(() => {
  console.log("child render"); // 父组件更新,子组件也会更新
  const { styleChild } = useStyle();
  return <div style={styleChild}>Child</div>;
});

如果要避免 memo 失效,有两种方式:

  1. 不要使用解构的语法,而是在 useStyle 中将 styleChildstate 中提取出来

    js 复制代码
    const useStyle = create((set) => ({
      styleChild: { color: "red" },
      styleParent: { color: "blue" },
      setStyleParent: (color) => set({ styleParent: { color } }),
    }));
    function App() {
      const [count, setCount] = useState(0);
      const { setStyleParent, styleParent } = useStyle();
      const onClick = useCallback(() => console.log("app click"), []);
      const onClickAppButton = () => {
        setCount(count + 1);
        setStyleParent(styleParent.color === "blue" ? "green" : "blue");
      };
      return (
        <div>
          app---{count}
          <button onClick={onClickAppButton} style={styleParent}>
            app 按钮
          </button>
          <Child onClick={onClick} />
        </div>
      );
    }
    const Child = memo(() => {
      console.log("child render"); // 父组件更新,子组件不会更新
      // 在 useStyle 中将 styleChild 从 state 中提取出来
      const styleChild = useStyle((state) => state.styleChild);
      return <div style={styleChild}>Child</div>;
    });
  2. 子组件中不使用 useStyle,而是将 styleChild 从父组件中传递过来

    js 复制代码
    const useStyle = create((set) => ({
      styleChild: { color: "red" },
      styleParent: { color: "blue" },
      setStyle: (color) => set({ styleParent: { color } }),
    }));
    function App() {
      const [count, setCount] = useState(0);
      const { setStyle, styleParent, styleChild } = useStyle();
      const onClick = useCallback(() => console.log("app click"), []);
      const onClickAppButton = () => {
        setCount(count + 1);
        setStyle(styleParent.color === "blue" ? "green" : "blue");
      };
      return (
        <div>
          app---{count}
          <button onClick={onClickAppButton} style={styleParent}>
            app 按钮
          </button>
          // 通过 props 传递 styleChild
          <Child onClick={onClick} style={styleChild} />
        </div>
      );
    }
    const Child = memo(({ style }) => {
      console.log("child render"); // 父组件更新,子组件不会更新了
      return <div style={style}>Child</div>;
    });

推荐使用第一种方式,毕竟使用第二种方式的话 zustand 就有点浪费了

缓存失效4------Context

使用 Context 后也会存在缓存失效的情况

context 提供 styleChildstyleParent 两个样式,styleChild 是子组件使用的样式,styleParent 是父组件使用的样式

子组件中如果直接使用 useContext 的话,就会导致 memo 失效

js 复制代码
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
  const [styleChild, setStyleChild] = useState({ color: "red" });
  const [styleParent, setStyleParent] = useState({ color: "blue" });
  const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
  return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
  <StyleProvider>
    <Parent />
  </StyleProvider>
);
const Parent = memo(() => {
  const [count, setCount] = useState(0);
  const { styleParent, setStyleParent, styleChild } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} style={styleChild} />
    </div>
  );
});
const Child = memo(({ style }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div style={style}>Child</div>;
});

解决 memo 失效的方式还是要通过 props 传值

js 复制代码
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
  const [styleChild, setStyleChild] = useState({ color: "red" });
  const [styleParent, setStyleParent] = useState({ color: "blue" });
  const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
  return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
  <StyleProvider>
    <Parent />
  </StyleProvider>
);
function Parent() {
  const [count, setCount] = useState(0);
  const { styleParent, setStyleParent, styleChild } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} style={styleChild} />
    </div>
  );
}
const Child = memo(({ style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新
  return <div style={style}>Child</div>;
});

总结

在开发过程中如果考虑性能优化的话,心智负担会太重,所以一般情况下不用考虑,只有在遇到性能问题时,再考虑优化

毕竟 react 自身已经做了很多优化了,比如 fiber 架构下的 setState 是异步的,react 会将多次 setState 合并成一次

所以在开发过程中,不需要考虑要不要使用 useCallbackuseMemomemo,遇到问题时在考虑也不迟

相关推荐
尾善爱看海20 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
驳是21 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
GreenTea21 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
星云API技术支持21 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
京东云开发者21 小时前
81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践
前端·aigc
Csvn1 天前
TypeScript 大型项目架构:从单体 tsconfig 到分层可扩展的工程
前端
计算机魔术师1 天前
Dario Amodei 发文呼吁为前沿 AI 降速并提出三点计划
前端
计算机魔术师1 天前
OpenAI的AI代理偷偷给RubyGems下毒,我们却毫无察觉
前端
甲维斯1 天前
0代码,0建模,3句话开发一个3D游戏!
前端·游戏·游戏开发
IT_陈寒1 天前
Vue的响应式更新把我坑惨了,原来问题出在这
前端·人工智能·后端