使用useMemo & useCallback进行性能优化

4,968

前言

react 没有新增 hooks 之前,我们可以通过PureComponentshouldComponentUpdate等方法进行性能优化,那么在hooks中有没有性能优化的方法呢?答案是肯定的。下面我们就来一一进行解答。

一、官方文档的解释

下面是官方文档的解释,感觉有些惜字如金,看完之后,依旧有一种找不到北的感觉...

参考链接 useCallback

useCallback

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

返回一个memoized回调函数。

内联回调函数依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

注意

依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

我们推荐启用 eslint-plugin-react-hooks 中的exhaustive-deps规则。此规则会在添加错误依赖时发出警告并给出修复建议。

useMemo

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

返回一个 memoized(记忆) 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

注意

依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。

我们推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议。

看完了关于useCallbackuseMemo的官方解释,大概可以总结出如下几点:

  1. useCallback接收一个内联回调函数和一个依赖项数组
  2. useMemo接收一个创建函数和一个依赖项数组
  3. useCallbackuseMemo都是在某个依赖项改变时才会更新
  4. 他们都是性能优化的方法
  5. useMemo用于计算量较大的地方

纸上得来终觉浅,绝知此事要躬行,下面我们就来写个例子

二、基础使用场景与对比

useCallback的使用

useCallback接收一个内联回调函数和一个依赖项数组,依赖项改变时才会更新。

1. 未使用useCallback

这个例子由两个组件构成,分别为<App />和子组件<Child />

function App () {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('fruit')
  const changeCount = () => {
    setCount(count + 1)
  }
  const changeName = () => {
    setName(name + 1)
  }


  return (
    <div>
      <div>Count is {count}</div>
       <div>Name is {name}</div>
      <br/>
      <div>
        <button onClick={changeName}>Change Name</button>
        <Child changeCount={changeCount} />
      </div>
    </div>
  )
}

function Child({changeCount}) {
  console.log('Child render')
  return <div>
    <button onClick={changeCount}>Increment Count</button>
  </div>
}

render(<App />, document.getElementById('root'))

stackblitz.com/edit/react-… 可以通过此在线编辑器进行尝试 如上图所示,当我们点击Change NameIncrement Count两个按钮时,都会导致,Child组件的重新渲染执行。就会造成非常大的开销。当点击Change Name的时候,并未更改Increment Count,但依旧会造成子组件重新执行。

2. 通过memo优化<Child />组件

之前的做法是用componentWillReceivePropsshouldComponentUpdatepureComponent等来做性能优化。在hooks中我们用memo来做优化。

改造后的<Child />如下:

const Child = memo(({changeCount}) => {
  console.log('Child render')
  return <div>
  <button onClick={changeCount}>Increment Count</button>
  </div>
})

但是此时我们点击Change NameIncrement Count两个按钮时,发现好像并没有什么用。

原因是什么呢?当我们将changeCount作为props传递给其他组件时会导致像PureComponent、shouldComponentUpdate、React.memo等相关优化失效(因为每次都是不同的函数)。

当我们传递changeCount的时候,每次我们点击按钮,函数都会被重新执行,就会创建新的changeCount每次的指针地址都不一样,因此就会导致每次的对比都不一样,因此我们还需要通过使用useCallback来解决这个问题。

memo类似于PureComponent,函数的props做一个浅比较,但useMemo是用来缓存值的,因此他们是不同的。

3. useCallback上场

function App () {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('fruit')
  // 这行是重点
  const changeCount = useCallback(() => {
    setCount(count + 1)
  }, [count])
  const changeName = () => {
    setName(name + 1)
  }


  return (
    <div>
      <div>Count is {count}</div>
       <div>Name is {name}</div>
      <br/>
      <div>
        <button onClick={changeName}>Change Name</button>
        <Child changeCount={changeCount} />
      </div>
    </div>
  )
}

const Child = memo(({changeCount}) => {
  console.log('Child render')
  return <div>
  <button onClick={changeCount}>Increment Count</button>
  </div>
})

stackblitz.com/edit/react-… 可以在这里进行试验,当点击Increment CountChild会正常执行,当点击Change Name时则Child不再执行。这样就达到了优化的目的

我们可以尽量对function包一层useCallback

因此,可以通过 memo + useCallback来达到不被附带更新的情况。但也会占用一定的内存。

useMemo的使用

useMemouseCallback在使用上几乎是一样的,但useMemo是用来缓存值的。

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的依赖我们就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。

useMemo创建函数作为依赖项数组传入useMemo,和useEffect有些类似,传入的函数改变只会依赖于传入依赖项数组中的值改变时会重新计算,重新执行函数,这个优化有助于避免在每次渲染的时候都进行高开销的计算。和useEffect的第二个参数一样,只有当第二个参数的值改变的时候才会进行计算。

三、源码解析

// useCallback
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;
}

// useMemo
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;
}

从上面的代码可以看出useMemouseCallback几乎是一样的,当我们理解了useCallback后理解useMemo就非常简单。 他们唯一的区别是:useCallback是根据依赖项(deps)缓存第一个参数callbackuseMemo是根据依赖项(deps)缓存第一个参数callback执行后的

useCallback会重新返回一个函数体,而useMemo返回的是一个缓存计算数据的值,当依赖项num变化时,usecallback会重新创建一个函数体,而useMemo不会。

反思

使用useMemouseCallback一定能达到优化的目的吗?

答案是否定的。

  1. useMemouseCallback不能盲目使用,因为他们都是基于闭包实现的,闭包会占用内存。
  2. 当依赖项频繁改动时,要考虑useMemo、useCallback是否划算,因为useCallback会频繁创建函数体。useMemo会频繁创建回调。

总结

  1. useMemouseCallback可以用来解决性能问题
  2. useCallback需要与memo配合使用
  3. useMemomemo是完全不同的
  4. useMemouseCallback的源码几乎是一样的,useCallback返回的是回调函数,useMemo返回的是值
  5. useMemo一般用于计算量比较大的情况
  6. useMemouseCallback不一定能达到性能优化的目的,因此不能盲目使用

就写到这里吧,如果错误欢迎指正...