关注 小贼先生,查看更多前端文章
Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性。
useCallback
和useMemo
是其中的两个 hooks,本文旨在经过解决一个需求,结合高阶函数,深刻理解useCallback
和useMemo
的用法和使用场景。
之因此会把这两个 hooks 放到一块儿说,是由于他们的主要做用都是性能优化,且使用useMemo
能够实现useCallback
。html
先把需求拎出来讲下,而后顺着需求往下捋useCallback
和useMemo
,这样更好理解为何要使用这两个 hooks。前端
需求是:当鼠标在某个 dom 标签上移动的时候,记录鼠标的普通移动次数和加了防抖处理后的移动次数。[如图]:react
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
,达到一段时间不活动后执行任务的目的。app
再来看看咱们的Example
组件,每次Example
组件的更新渲染,都会经过debounce(setBounceCount)
生成一个新的debounceSetCount
,也就是每次的更新渲染,debounceSetCount
都是指向不一样的debounced
,不一样的debounced
使用着不一样的timer
,那么debounce
函数里的闭包就失去了意义,因此才会出现截图中的状况。dom
可是,为何bounceCount
的值看着像是进行过防抖处理同样呢?
由于debounceSetCount(bounceCount + 1)
在屡次执行时,debounce
内的setTimeout
。setBounceCount
是在setTimeout
内执行的,也就是异步的,等待时间约是1000ms,而handleMouseMove
虽然是事件回调函数,但鼠标移动时,这个函数的执行间隔相比1000ms要短不少,最终使得bounceCount
参数值老是相同的,因此整个效果才像通过了防抖处理同样。异步
咱们使用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
如何解决这个问题呢:
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
,你有什么好的解决方案呢,欢迎留言讨论。
关注 小贼先生,查看更多前端文章