换个角度,结合高阶函数聊聊React的useMemo和useCallback

关注 小贼先生,查看更多前端文章
Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。

useCallbackuseMemo是其中的两个 hooks,本文旨在经过解决一个需求,结合高阶函数,深刻理解useCallbackuseMemo的用法和使用场景。
之因此会把这两个 hooks 放到一块儿说,是由于他们的主要做用都是性能优化,且使用useMemo能够实现useCallbackhtml

需求说明

先把需求拎出来讲下,而后顺着需求往下捋useCallbackuseMemo,这样更好理解为何要使用这两个 hooks。前端

需求是:当鼠标在某个 dom 标签上移动的时候,记录鼠标的普通移动次数和加了防抖处理后的移动次数。[如图]:
react

技术储备

  • 本文主要介绍useCallbackuseMemo,因此遇到useState时就不作特殊说明了,若是对useState还不了解,请参看官方文档
  • 该需求须要用到防抖函数,为方便调试,先准备一个简单的防抖函数(一个高阶函数):
function debounce(func, delay = 1000) {
  let timer;

  function debounced(...args) {
    debounced.cancel();
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  }

  debounced.cancel = function () {
    if (timer !== undefined) {
      clearTimeout(timer);
      timer = undefined;
    }
  }
  return debounced
}

不合格的解决方案

根据需求,写出来组件大体会是这样:数组

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = debounce(setBounceCount);

  const handleMouseMove = () => {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove}>
      <p>普通移动次数: {count}</p>
      <p>防抖处理后移动次数: {bounceCount}</p>
    </div>
  )
}

效果貌似是对的,在debounced里打印日志看下:缓存

function debounce(func, delay = 1000) {
    // ... 省略其余代码
    timer = setTimeout(() => {
      // 在此处添加了一行打印代码
      console.log('run-do');
      func.apply(this, args);
    }, delay);
    // ... 省略其余代码
}

当鼠标在div标签上移动时,打印结果[如图]:性能优化

咱们发现,当鼠标中止移动后,run-do被打印的次数,跟鼠标移动次数相同,这说明防抖功能并未生效。是哪里出问题了呢?闭包

首先咱们要清楚的是,使用debounce的目的是经过debounce返回一个debounced函数(注意:此处是debounced,而不是debounce,下文一样要注意这个细节,不然意思就彻底不对了),而后每次执行debounced时,经过闭包内的timer清掉以前的setTimeout,达到一段时间不活动后执行任务的目的。app

再来看看咱们的Example组件,每次Example组件的更新渲染,都会经过debounce(setBounceCount)生成一个新的debounceSetCount,也就是每次的更新渲染,debounceSetCount都是指向不一样的debounced,不一样的debounced使用着不一样的timer,那么debounce函数里的闭包就失去了意义,因此才会出现截图中的状况。dom

可是,为何bounceCount的值看着像是进行过防抖处理同样呢?
由于debounceSetCount(bounceCount + 1)在屡次执行时,debounce内的setTimeoutsetBounceCount是在setTimeout内执行的,也就是异步的,等待时间约是1000ms,而handleMouseMove虽然是事件回调函数,但鼠标移动时,这个函数的执行间隔相比1000ms要短不少,最终使得bounceCount参数值老是相同的,因此整个效果才像通过了防抖处理同样。异步

useCallback

咱们使用useCallback修改下咱们的组件:

function Example() {
  // ... 省略其余代码
  // 相比以前的 Example 组件,咱们只是增长了 useCallback hook
  const debounceSetCount = React.useCallback(debounce(setBounceCount), []);
  // ... 省略其余代码
}

这时再用鼠标在div标签上移动时,效果跟咱们的需求一致了,[如图]:

经过useCallback,咱们貌似解决了以前存在的问题(其实这里面还有问题,咱们后面会说到)。

那么,useCallback是怎么解决问题的呢?
看下useCallback的调用签名:

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

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

经过useCallback的签名能够知道,useCallback第一个参数是一个函数,返回一个 memoized 回调函数,如上面代码中的 memoizedCallback 。useCallback的第二个参数是依赖(deps),当依赖改变时才更新 memoizedCallback ,也就是在依赖未改变时(或空数组无依赖时), memoizedCallback 老是指向同一个函数,也就是指向同一块内存区域。当把 memoizedCallbac 看成 props 传递给子组件时,子组件就能够经过shouldComponentUpdate等手段避免没必要要的更新。

Example组件首次渲染时,debounceSetCount的值是debounce(setBounceCount)的执行结果,由于经过useCallback生成debounceSetCount时,传入的依赖是空数组,因此Example组件在下一次渲染时,debounceSetCount会忽略debounce(setBounceCount)的执行结果,老是返回Example第一次渲染时useCallback缓存的结果,也就是说debounce(setBounceCount)的执行结果经过useCallback缓存了下来,解决了debounceSetCountExample每次渲染时老是指向不一样debounced的问题。

咱们上面说过,这里面其实还有一个问题,那就是每次Example组件更新的时候,debounce函数都会执行一次,经过上面的分析咱们知道,这是一次无用的执行,若是此处的debounce函数里有大量的计算的话,就会很影响性能。

useMemo

看下使用useMemo如何解决这个问题呢:

function Example() {
  const [count, setCount] = useState(0);
  const [bounceCount, setBounceCount] = useState(0);
  const debounceSetCount = React.useMemo(() => debounce(setBounceCount), []);

  const handleMouseMove = () => {
    setCount(count + 1);
    debounceSetCount(bounceCount + 1);
  };

  return (
    <div onMouseMove={handleMouseMove} >
      <p>普通移动次数: {count}</p>
      <p>防抖处理后移动次数: {bounceCount}</p>
    </div>
  )
}

如今,每次Example更新渲染时,debounceSetCount都是指向同一块内存,并且debounce只会执行一次,咱们的需求完成了,咱们的问题也都获得了解决。

小贼先生-文章原址

useMemo是怎么作到的呢?
看下useMemo的调用签名:

function useMemo<T>(factory: () => T, deps: ReadonlyArray<any> | undefined): T;

// 示例:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

经过useMemo的签名能够知道,useMemo第一个参数是一个 factory 函数,该函数的返回结果会经过useMemo缓存下来,只有当useMemo的依赖(deps)改变时才从新执行 factory 函数,memoizedValue 才会被从新计算。 也就是在依赖未改变时(或空数组无依赖时),memoizedValue 老是返回经过useMemo缓存的值。

看到这里,相信细心的你也已经发现了,useCallback(fn, deps) 其实至关于 useMemo(() => fn, deps),因此在最开始咱们说:使用useMemo彻底能够实现useCallback

特别注意

React 官方有这么一句话:
你能够把 useMemo 做为性能优化的手段,但不要把它当成语义上的保证。未来,React 可能会选择“遗忘”之前的一些 memoized 值,并在下次渲染时从新计算它们,好比为离屏组件释放内存。先编写在没有 useMemo 的状况下也能够执行的代码 —— 以后再在你的代码中添加 useMemo,以达到优化性能的目的。 查看原文

显然,咱们的代码中,若是去掉useMemo是会出问题的,对此,可能有人会想,改装下debounce防抖函数就能够了,例如:

function debounce(func, ...args) {
  if (func.timeId !== undefined) {
    clearTimeout(func.timeId);
    func.timeId = undefined;
  }

  func.timeId = setTimeout(() => {
    func(...args);
  }, 200);
}

// 使用 useCallback
function Example() {
  // ... 省略其余代码
  const debounceSetCount = React.useCallback((...args) => {
    debounce(setBounceCount, ...args);
  }, []);
  // ... 省略其余代码
}

// 不使用 useCallback
function Example() {
  // ... 省略其余代码
  const debounceSetCount = changeCount => debounce(setBounceCount, changeCount);
  // ... 省略其余代码
}

貌似去掉了useMemo也能实现咱们的需求,但显然,这是一种很是将就的解决方案,一旦遇到像修改前的debounce这样的高阶函数就一筹莫展了。
那么,若是不使用useMemo,你有什么好的解决方案呢,欢迎留言讨论。

关注 小贼先生,查看更多前端文章

参考文档

相关文章
相关标签/搜索