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,遇到问题时在考虑也不迟

相关推荐
喵叔哟30 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕2 小时前
Django 搭建数据管理web——商品管理
前端·python·django