memo、useCallback、useMemo 傻傻分不清

·  阅读 951

前言

memo、useCallback、useMemo 是不是很多时候都傻傻分不清楚?

看完这篇文章,快速理解它们各自的使用场景,以一个实际开发场景为例:在使用 React 进行开发的时候,优化渲染是一项常备技能。最常见的是父组件渲染的时候,连带的子组件也会重新执行。

例如:

// in App.js
import React, { useState, useMemo, memo } from "react";
import ChildComponent from "./ChildComponent";

export default function App() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>{count}</h2>
      <button onClick={handleClick}>click</button>
      <ChildComponent />
    </div>
  );

// in ChildComponent.js
const ChildComponent = (props) => {
  console.log("ChildComponent Running");
  return <div>{`这里是 ChildComponent`}</div>;
};

export default ChildComponent;
复制代码

每次点击后,父元素中的 state 都会发生变化,但是子元素并不依赖于父元素的 state,但是子元素同样会重新执行一遍。

React.PureComponent

如果是使用 class 来实现的组件,可以使用 React.PureComponent 来定义组件。

class Greeting extends React.PureComponent {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
复制代码

React.PureComponent 与 React.Component 区别在于:PureComponent 实现了 shouldComponentUpdate 方法,可以对 props 和 state 进行浅比较,从而优化组件渲染。

React.Memo

使用 React.memo 将组件包裹。React.memo 包裹后的组件将在 props 不变的情况下,省略组件渲染的操作直接复用最近一次的渲染结果。React.memo 默认对复杂对象做浅层比较,也可以自定义比较函数作为第二个参数传递给 React.memo。

const ChildComponentMemo = memo(ChildComponent, (prevProps, nextProps) => {
	// return true or false;
});
复制代码

useCallback

但是 React.memo 有缺陷。如果传入的是一个引用数据类型,那么在修改与子组件无关的 state 时, ChildComponet 依然会重新执行。

// in ChildComponent.js
const ChildComponent = (props) => {
  console.log("ChildComponent Running");
  return (
    <div>
      {`这里是 ChildComponent`}
      <button onClick={props.handleAddCat}>add cat</button>
    </div>
  );
};

// in APP.js
export default function App() {
  const [dogCount, setDogCount] = useState(0);
  const [catCount, setCatCount] = useState(0);

  const handleAddDog = () => {
    setDogCount(dogCount + 1);
  };

  // 这个函数要传给子组件
  const handleAddCat = () => {
    setCatCount(catCount + 1);
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>dog:{dogCount}</h2>
      <h2>cat:{catCount}</h2>
      <button onClick={handleAddDog}>click</button>
      <ChildComponentMemo handleAddCat={handleAddCat}/>
    </div>
  );
}
复制代码

这是因为 click 后 state 发生变化,那么父组件(App.js)会重新执行,导致引用类型会被重新创建(引用地址发生改变),子组件(ChildComponent.js)会认为 props 发生改变,从而重新执行。

这时候需要使用 useCallback 来为引用类型地址做一个缓存:

// in APP.js
export default function App() {
  const [dogCount, setDogCount] = useState(0);
  const [catCount, setCatCount] = useState(0);

  const handleAddDog = () => {
    setDogCount(dogCount + 1);
  };

  // dogCount 改变后,handleAddCat 的地址不会发生改变
  // ChildComponent 也不会重新执行
  const handleAddCat = useCallback(() => {
    setCatCount(catCount + 1);
  }, [catCount]);

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>dog:{dogCount}</h2>
      <h2>cat:{catCount}</h2>
      <button onClick={handleAddDog}>click</button>
      <ChildComponentMemo handleAddCat={handleAddCat}/>
    </div>
  );
}
复制代码

useMemo

很多人会问 useMemo 和 useCallback 有什么区别?

一句话总结:useCallback 缓存的是函数,useMemo 缓存的是函数执行后的值。

即 useCallback(fn, [deps]) 等价于 useMemo(() => fn, [deps])

// in App.js
function App() {
  const [dogCount, setDogCount] = useState(0);
  const [catCount, setCatCount] = useState(0);
  
  const handleAddDog = () => {
    setDogCount(dogCount + 1);
  };
  
	const computeValue = (catCount) => {
    // 假设这里有大量计算
    console.log('computeValue Running');
    
    return catCount + 1;
  }

  // 当 dogCount 发生变化,这里会重新执行 computeValue 函数
  const value = computeValue(catCount);
  console.log('value: ', value);
}
复制代码

当点击 click 增加 dogCount 数量时,computeCount 依然会执行,但是它的计算与 dogCount 毫无关系,其执行的结果是一样的。

这时候可以用 useMemo 将计算值缓存起来:

// in App.js
function App() {
  const [dogCount, setDogCount] = useState(0);
  const [catCount, setCatCount] = useState(0);
  
  const handleAddDog = () => {
    setDogCount(dogCount + 1);
  };
  
	const computeValue = (catCount) => {
    // 假设这里有大量计算
    console.log('computeValue Running');
    
    return catCount + 1;
  }

  // 使用 useMemo 后,只有当 catCount 变化时,才会重新计算得到 value
  // 如果 deps 为空,那么只有在第一次渲染时执行
  const value = useMemo(() => computeValue(catCount), [catCount])
}
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改