结合高阶函数聊聊useMemo和useCallback

更新日期: 2019-10-18阅读: 3.8k标签: Hook
Hook 是 react 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

useCallback和useMemo是其中的两个 hooks,本文旨在通过解决一个需求,结合高阶函数,深入理解useCallback和useMemo的用法和使用场景。 之所以会把这两个 hooks 放到一起说,是因为他们的主要作用都是性能优化,且使用useMemo可以实现useCallback。


需求说明

先把需求拎出来说下,然后顺着需求往下捋useCallback和useMemo,这样更好理解为什么要使用这两个 hooks。需求是:当鼠标在某个 dom 标签上移动的时候,记录鼠标的普通移动次数和加了防抖处理后的移动次数。


技术储备

本文主要介绍useCallback和useMemo,所以遇到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,达到一段时间不活动后执行任务的目的。

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

但是,为什么bounceCount的值看着像是进行过防抖处理一样呢?
那是debounceSetCount(bounceCount + 1)在多次执行时,因为debounce内的setTimeout使得bounceCount参数值是相同的,所以通过run-do的打印次数才把问题暴露了出来。


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缓存了下来,解决了debounceSetCount在Example每次渲染时总是指向不同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这样的高阶函数就束手无策了。 

小贼先生-文章原址 


链接: https://www.fly63.com/article/detial/6117

分享 10 个可以使用 Vue.js 制作的有用的自定义钩hook

Vue.js 是我使用的第一个 JavaScript 框架。 我可以说 Vue.js 是我进入 JavaScript 世界的第一扇门之一。 目前,Vue.js 仍然是一个很棒的框架。 我认为有了组合 API,Vue.js 只会增长得更多

pytest插件探索——hook开发

conftest.py可以作为最简单的本地plugin调用一些hook函数,以此来做些强化功能。pytest整个框架通过调用如下定义良好的hooks来实现配置,收集,执行和报告这些过程:

React中useState Hook 示例

到 React 16.8 目前为止,如果编写函数组件,然后遇到需要添加状态的情况,咱们就必须将组件转换为类组件。编写 class Thing extends React.Component,将函数体复制到render()方法中,修复缩进,最后添加需要的状态。

useContext Hook 是如何工作的?

所有这些新的React Hook之间都有一个宗旨:就是为了使函数组件像类组件一样强大。useContext hook 与其它几个有点不一样,但它在特定场景下还是很有用的。React 的 Context API 是一种在应用程序中深入传递数据的方法

结合React的Effect Hook分析组件副作用的清除

我们在DidMount的时候通过ID订阅了好友的在线状态,并且为了防止内存泄漏,我们需要在WillUnmount清除订阅,但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么?

关于为什么使用React新特性Hook的一些实践与浅见

Hook是对函数式组件的一次增强,使得函数式组件可以做到class组件的state和生命周期。Hook的语法更加简练易懂,消除了class的生命周期方法导致的重复逻辑代码,解决了高阶组件难以理解和使用困难的问题。

React封装强业务hook的一个例子

最近因为使用列表展示的需求有点多,就想着把列表分页筛选的逻辑抽象一下。看了umi的一个useTable的hook,也不能满足业务需要,于是就自己写了一个,支持本地分页筛选和接口分页筛选。

React官方团队出手,补齐原生Hook短板

然而实际上,由于回调函数被useCallback缓存,形成闭包,所以点击的效果始终是sendMessage()。这就是「闭包陷阱」。以上代码的一种解决方式是「为useCallback增加依赖项」

实现一个自定义 React Hook:UseLocalStorageState

最近做需求,需要将数据保存到 localStorage 里,在组件初始化的时候获取,然后修改该值的时候,要保存到本地的 localStorage 中。很显然,这些逻辑完全可以封装为一个 React Hook

为什么Hook没有ErrorBoundary?

在很多全面使用Hooks开发的团队,唯一使用ClassComponent的场景就是使用ClassComponent创建ErrorBoundary。可以说,如果Hooks存在如下两个生命周期函数的替代品,就能全面抛弃ClassComponent了:

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!