超性感的React Hooks(十一)useCallback、useMemo

在实践开发中,有一种优化手段叫做记忆函数

什么是记忆函数?用一个例子来说明。

我们想要计算从1到某个整数的总和。封装一个方法来实现这个目的。

function summation (target) {
  let sum = 0
  for(let i = 1; i <= sum; i++) {
      sum += i
  }
  return sum
}

验证一下结果,没有问题。
WeChatab5fa4f69f6ed14957fac120cbbb10b8.png

这个时候,我们思考一个问题,当我们重复调用summation(100)时,函数内部的循环计算是不是有点冗余?因为传入的参数一样,得到的结果必定也是一样,因此如果传入的参数一致,是不是可以不用再重复计算直接用上次的计算结果返回呢?

当然可以,利用闭包能够实现我们的目的。

// 初始化⼀个⾮正常数字,⽤于缓存上⼀次的计算结果
let preTarget = -1;
let memoSum = 0;
export function memoSummation(target: number) {
  // 如果传⼊的参数与上⼀次⼀样,直接换回缓存结果
  if (preTarget > 0 && preTarget === target) {
    return memoSum;
  }
  console.log('我出现,就表示重新计算了⼀次');
  // 缓存本次传⼊的参数
  preTarget = target;
  let sum = 0;
  for (let i = 1; i <= target; i++) {
    sum += i;
  }
  // 缓存本次的计算结果
  memoSum = sum;
  return sum;
}

多次调⽤ memoSummation(1000) ,没有问题,我们的⽬的达到了。后两次的调⽤直接返回了记忆中
的结果。

WeChat4e5e40bf461c439c8a8cf0b8eaf2decd.png

记忆函数利用闭包,在确保返回结果一定正确的情况下,减少了重复冗余的计算过程

  1. react hooks提供的api,大多都有记忆功能。例如
    useState
    useEffect
    useLayoutEffect
    useReducer
    useRef
    useMemo 记忆计算结果
    useCallback 记忆函数体

    其他⼏个api的使⽤⽅法,我们在前⾯已经⼀⼀跟⼤家分析过。这⾥主要关注useMemo与
    useCallback。

useMemo
useMemo缓存计算结果。它接收两个参数,第一个参数为计算过程(回调函数,必须返回一个结果),第二个参数是依赖项(数组),当依赖项中某一个发生变化,结果将会重新计算。

// allow undefined, but don't make it optional as that is very likely a mistake
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;

useCallback
useCallback的使用几乎与useMemo一样,不过useCallback缓存的是一个函数体,当依赖项中的一项发现变化,函数体会重新创建。

1 function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

写⼀个案例,来观察⼀下他们的使⽤。

import React, { useMemo, useState, useCallback } from 'react';
import { Button } from 'antd-mobile';

export default function App() {
  const [target, setTarget] = useState(0);
  const [other, setOther] = useState(0)

  const sum = useMemo(() => {
  console.log('重新计算⼀次');
  let _sum = 0;
  for (let i = 1; i <= target; i++) {
    _sum += i;
  }
    return _sum;
  }, [target]);

  const inputChange = useCallback((e) => {
    console.log(e.target.value);
  }, []);
  
  return (
  <div style={{ width: '200px', margin: 'auto' }}>
    <input type="text" onChange={inputChange} />
    <div style={{ width: '80px', margin: '100px auto', fontSize: '40px' }}>{target} {sum}</div>
    <Button onClick={() => setTarget(target + 1)}>递增</Button>
    <Button onClick={() => setTarget(target - 1)}>递减</Button>
    <div style={{ width: '80px', margin: '100px auto', fontSize: '20px' }}>⼲扰项 {other}</div>
    <Button onClick={() => setOther(other + 1)}>递增</Button>
  </div>
  )
}

useMemo/useCallback 的使⽤⾮常简单,不过我们需要思考⼀个问题,使⽤他们⼀定能够达到优化
的⽬的吗?
React的学习经常容易陷⼊过度优化的误区。⼀些⼈在得知 shouldComponentUpdate 能够优化性
能,恨不得每个组件都要⽤⼀下,不⽤就感觉⾃⼰的组件有问题。 useMemo/useCallback 也是⼀
样。
明⽩了记忆函数的原理,我们应该知道,记忆函数并⾮完全没有代价,我们需要创建闭包,占
⽤更多的内存,⽤以解决计算上的冗余。
useMemo/useCallback 也是⼀样,这是⼀种成本上的交换。那么我们在使⽤时,就必须要思考,这
样的交换,到底值不值?
如果不使⽤useCallback,我们就必须在函数组件内部创建超多的函数,这种情况是不是就⼀定
有性能问题呢?
不是的。
我们知道,⼀个函数执⾏完毕之后,就会从函数调⽤栈中被弹出,⾥⾯的内存也会被回收。因
此,即使在函数内部创建了多个函数,执⾏完毕之后,这些创建的函数也都会被释放掉。函数
式组件的性能是⾮常快的。相⽐class,函数更轻量,也避免了使⽤⾼阶组件、renderProps等
会造成额外层级的技术。使⽤合理的情况下,性能⼏乎不会有什么问题。

那么,什么时候使用useMemo/useCallback比较合适
总的原则,就是当你认为,交换能够赚的时候去使用它们。
例如在一个一定会多次rerender的组件里,input的回调没有任何依赖项,我们就可以使用useCallback来降低多次执行带来的重复创建同样方法的负担。
即使这样,也可能并不会优化多少,因为我们缓存的函数体本身就非常简单,不会造成太大的负担

<input type="text" onChange={inputChange} />

const inputChange = useCallback((e) => {
 setValue(e.target.value);
}, []);

但是,同样的场景,如果该组件⼀定只会渲染⼀次,那么使⽤useCallback就完全没有必要。
通常情况下,当函数体或者结果的计算过程⾮常复杂时,我们才会考虑优先使⽤
useCallback/useMemo。
例如,在⽇历组件中,需要根据今天的⽇期,计算出当⽉的所有天数以及相关的信息
当依赖项会频繁变动时,我们也要考虑使用useMemo/useCallback是否划算
每当依赖项变动,useMemo/useCallback不会直接返回计算结果,这个时候,结果会重新计算,函数体会重新创建。因此依赖项变动频繁时,需要慎重考虑。
最后,一图总结全文。
WeChat578364a6babc53cc6a0a1f7258003058.png


关于作者

royxu
获得点赞
文章被阅读