useCallback、useMemo 分析 & 差别

21,359 阅读5分钟

结论

先说结论useCallbackuseMemo都可缓存函数的引用或值,但是从更细的使用角度来说useCallback缓存函数的引用,useMemo缓存计算数据的值。

回顾

useCallback回顾

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

根据官网文档的介绍我们可理解:在ab的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。

useMemo回顾

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

根据官方文档的介绍我们可理解:在ab的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。

分析

useCallback分析

我做了这样一个简单示例,我们来分析一下现象。

// 在Hooks中获取上一次指定的props
const usePrevProps = value => {
  const ref = React.useRef();
  React.useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function App() {
  const [count, setCount] = React.useState(0);
  const [total, setTotal] = React.useState(0);
  const handleCount = () => setCount(count + 1);
  const handleTotal = () => setTotal(total + 1);
  const prevHandleCount = usePrevProps(handleCount);
  
  console.log('两次处理函数是否相等:', prevHandleCount === handleCount);
  
  return (
    <div>
      <div>Count is {count}</div>
       <div>Total is {total}</div>
      <br/>
      <div>
        <button onClick={handleCount}>Increment Count</button>
        <button onClick={handleTotal}>Increment Total</button>
      </div>
    </div>
  )
}

ReactDOM.render(<App />, document.body)

我们重点看这一行

const handleCount = () => setCount(count + 1);

根据我们之前的理解,我们知道每次App组件渲染时这个handleCount都是重新创建的一个新函数。

const prevHandleCount = usePrevProps(handleCount);  
console.log('两次处理函数是否相等:', prevHandleCount === handleCount);

我们也可以通过比较上一次的prevHandleCount和本次的handleCount。可以明确的知道每次渲染时handleCount都是重新创建的一个新函数。

问题:它有什么问题呢?当我们将handleCount作为props传递给其他组件时会导致像PureComponentshouldComponentUpdateReact.memo等相关优化失效(因为每次都是不同的函数)

展示问题Gif:

1

为了解决上述的问题,我们需要引入useCallback,通过使用它的依赖缓存功能,在合适的时候将handleCount缓存起来。我创建了一个简单示例,来看看是如何解决的吧。

// 在Hooks中获取上一次指定的props
const usePrevProps = value => {
  const ref = React.useRef();
  React.useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function App() {
  const [count, setCount] = React.useState(0);
  const [total, setTotal] = React.useState(0);
  const handleCount = React.useCallback(() => setCount(count => count + 1), []);
  const handleTotal = () => setTotal(total + 1);
  const prevHandleCount = usePrevProps(handleCount);
  
  console.log('两次处理函数是否相等:', prevHandleCount === handleCount);
  
  return (
    <div>
      <div>Count is {count}</div>
       <div>Total is {total}</div>
      <br/>
      <div>
        <button onClick={handleCount}>Increment Count</button>
        <button onClick={handleTotal}>Increment Total</button>
      </div>
      <AotherComponent onClick={handleCount} />
    </div>
  )
}

const AotherComponent = React.memo(function AotherComponent({ onClick }) {
  console.log('AotherComponent 组件渲染');
  return (
    <button onClick={onClick}>AotherComponent - Inrement Count</button>
  )
})

ReactDOM.render(<App />, document.body)

这次我们重点看这行

const handleCount = React.useCallback(() => setCount(count => count + 1), []);

我使用useCallback来缓存了函数,依赖项(deps)是一个空数组它代表这个函数在组件的生成周期内会永久缓存

const AotherComponent = React.memo(function AotherComponent({ onClick }) {
  console.log('AotherComponent 组件渲染');
  return (
    <button onClick={onClick}>AotherComponent - Inrement Count</button>
  )
})

因为我们的handleCount是一个缓存函数,所以当我们传递给经过React.memo优化的组件AotherComponent时不会触发渲染

温馨提示:在选择useCallback的依赖(deps)时请经过仔细的考虑,比如下面这样的依赖是达不到最好的优化效果,因为当我们增加了一次count时,handleCount的引用就会更改。

解决问题Gif,我们可以看到prevHandleCount等于handleCount,也没有多余的渲染:

2

const handleCount = React.useCallback(() => setCount(count => count + 1), [count]);

useMemo分析

useMemouseCallback几乎是99%像是,当我们理解了useCallback后理解useMemo就非常简单。

他们的唯一区别就是:useCallback是根据依赖(deps)缓存第一个入参的(callback)。useMemo是根据依赖(deps)缓存第一个入参(callback)执行后的值。

你明白了吗? 如果还没明白我贴一下useCallbackuseMemo的源码你来看看区别。

// 注:为了方便理解我省去了一些flow语法

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate(); // 🤩
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

聪明的你一定看出来区别是啥了对吧。

useMemo一般用于密集型计算大的一些缓存。

下面我写了一个简单示例,来展示useMemo如何使用的。

// 在Hooks中获取上一次指定的props
const usePrevProps = value => {
  const ref = React.useRef();
  React.useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

function App() {
  const [count, setCount] = React.useState(0);
  const [total, setTotal] = React.useState(0);
  const calcValue = React.useMemo(() => {
    return Array(100000).fill('').map(v => /*一些大量计算*/ v);
  }, [count]);
  const handleCount = () => setCount(count => count + 1);
  const handleTotal = () => setTotal(total + 1);
  const prevCalcValue = usePrevProps(calcValue);
  
  console.log('两次计算结果是否相等:', prevCalcValue === calcValue);
  return (
    <div>
      <div>Count is {count}</div>
       <div>Total is {total}</div>
      <br/>
      <div>
        <button onClick={handleCount}>Increment Count</button>
        <button onClick={handleTotal}>Increment Total</button>
      </div>
    </div>
  )
}

ReactDOM.render(<App />, document.body)

这次我们重点看这行,只有当count变量值改变的时候才会执行useMemo第一个入参的函数。

const calcValue = React.useMemo(() => {
    return Array(100000).fill('').map(v => /*一些大量计算*/ v);
  }, [count]);

通过useMemo的依赖我们就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。

总结

我们一起回顾了useCallbackuseMemo的基本使用方法,接着找到了影响性能的根本原因然后通过useCallback如何去解决性能问题。最后我们学习了如何使用useMemo去缓存了计算量密集的函数。我们还通过观察React Hooks源码观察了useCallbackuseMemo最根本的区别,这让我们在开发时可以做出正确的选择。

如果有错误请斧正

感谢阅读