Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useCallback、useMemo 分析 & 差别 #37

Open
monsterooo opened this issue Nov 21, 2019 · 13 comments
Open

useCallback、useMemo 分析 & 差别 #37

monsterooo opened this issue Nov 21, 2019 · 13 comments

Comments

@monsterooo
Copy link
Owner

monsterooo commented Nov 21, 2019

结论

先说结论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

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最根本的区别,这让我们在开发时可以做出正确的选择。

如果有错误请斧正

感谢阅读

@monsterooo monsterooo changed the title useCallback和useMemo分析和差别 useCallback & useMemo分析和差别 Nov 21, 2019
@monsterooo monsterooo changed the title useCallback & useMemo分析和差别 useCallback & useMemo 分析 & 差别 Nov 21, 2019
@monsterooo monsterooo changed the title useCallback & useMemo 分析 & 差别 useCallback、useMemo 分析 & 差别 Nov 21, 2019
@NsNe
Copy link

NsNe commented Dec 19, 2019

这篇文章是我看到useCallback和useMemo讲的比较好的,可以转载否?另外,直接加上codepen的代码例子,就更好了

@monsterooo
Copy link
Owner Author

monsterooo commented Dec 19, 2019

这篇文章是我看到useCallback和useMemo讲的比较好的,可以转载否?另外,直接加上codepen的代码例子,就更好了

嗯,可以转载的哈。

上面的代码部份是有codepen演示的哈,仔细看应该能发现😀

@AlexZhong22c
Copy link

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

该句加了[count]之后,handleCount每次的值都会变。这代码其实没有做到性能优化。

@monsterooo
Copy link
Owner Author

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

该句加了[count]之后,handleCount每次的值都会变。这代码其实没有做到性能优化。

是的,正确的应该是这样使用
const handleCount = React.useCallback(() => setCount(count => count + 1), []);

@AlexZhong22c
Copy link

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

这样还是有问题,useCallback绑定的回调函数拿不到最新的count的值

@NsNe
Copy link

NsNe commented Dec 30, 2019

@monsterooo

我同意@AlexZhong22c的说法。

  • 这个依赖是要写的,useCallback只是说当依赖不变的时候,返回同一个函数,但是不能省略依赖的。

  • useCallback应该是说避免了其他的state对当前函数的影响。这个count的依赖项我认为是必须要的。

  • 另外呢,还可以用useReducer去维护一个局部的redux,dispatch去做,见useReducer

@monsterooo
Copy link
Owner Author

monsterooo commented Dec 30, 2019

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

这样还是有问题,useCallback绑定的回调函数拿不到最新的count的值

感觉你还是没理解,count可以拿到最新的值(闭包作用域), 在这个例子中已经很明确的展示出来了。

如果还有撒疑问可以继续讨论

@NsNe
Copy link

NsNe commented Dec 30, 2019

@monsterooo

我同意@AlexZhong22c的说法。

  • 这个依赖是要写的,useCallback只是说当依赖不变的时候,返回同一个函数,但是不能省略依赖的
  • useCallback应该是说避免了其他的state对当前函数的影响。这个count的依赖项我认为是必须要的。
  • 另外呢,还可以用useReducer去维护一个局部的redux,dispatch去做,见useReducer

我看错了, setCount(count => count + 1) setCount是个函数,可以拿到最新的值。相当于以前的setState(prevState => prevState.count + 1)。
不过,useCallback(() => setCount(count => count + 1), [])第二个参数写空数组不合适的吧,毕竟官方文档描述认为函数中引用的项都应该出现在依赖中

@monsterooo
Copy link
Owner Author

@monsterooo

我同意@AlexZhong22c的说法。

  • 这个依赖是要写的,useCallback只是说当依赖不变的时候,返回同一个函数,但是不能省略依赖的
  • useCallback应该是说避免了其他的state对当前函数的影响。这个count的依赖项我认为是必须要的。
  • 另外呢,还可以用useReducer去维护一个局部的redux,dispatch去做,见useReducer

我看错了, setCount(count => count + 1) setCount是个函数,可以拿到最新的值。相当于以前的setState(prevState => prevState.count + 1)。
不过,useCallback(() => setCount(count => count + 1), [])第二个参数写空数组不合适的吧,毕竟官方文档描述认为函数中引用的项都应该出现在依赖中

最佳实践还是按照官方的来没问题,因为那个比较适合大多数情况。

像我们讨论的这种情况(优化依赖和缓存函数),不是应用特别大或者组件特别多的时候不会有太大的问题。

但是如果我们在写代码之前就知道并且做一些优化,对于以后可能会出现的性能问题就会有所避免。

@chj-damon
Copy link

如果useCallback或者useMemo的依赖是引用对象呢?比如Object/Array,如果没有记错的话,每次它们都会触发重新计算

This was referenced Jun 18, 2020
@zsjun
Copy link

zsjun commented Jun 18, 2020

React.useEffect(() => {
ref.current = value;
});
这些尽量都写上依赖,其实不写和写也是一样的

@powerdong
Copy link

https://jancat.github.io/post/2019/translation-usememo-and-usecallback/

useCallback 和 useMemo的成本是:对于你的同事来说,你使代码更复杂了;你可能在依赖项数组中犯了一个错误,并且你可能通过调用内置的 hook、并防止依赖项和 memoized 值被垃圾收集,而使性能变差。如果你获得了必要的性能收益,那么这些成本都是值得承担的,但最好先测量一下。

@delaprada
Copy link

如果useCallback或者useMemo的依赖是引用对象呢?比如Object/Array,如果没有记错的话,每次它们都会触发重新计算

同问,如果是对象的话会怎么比较呢?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants