记忆函数 在开发实践中,有一种优化手段叫做记忆函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function summation (target ) { let sum = 0 ; for (let i = 1 ; i <= target; i++) { sum += i; } console .log ('sum: ' , sum); return sum; } summation (10 );summation (100 );summation (100 );summation (100 );summation (100 );
这是一个求和函数,每次执行函数都会重新计算结果。当我们重复调用summation(100)
时,内部的循环计算是不是有点冗余?因为传入相同的参数,必定得到相同的结果,因此如果传入参数一样,是不是可以不再重复计算直接使用上次的计算结果呢?
是的,利用闭包能够实现我们的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let preTarget = -1 ;let memoSum = 0 ;export function memoSummation (target ) { if (prevTarget > 0 && prevTarget === target) { return memoSum; } prevTarget = target; let sum = 0 ; console .log ('我出现,表示又重新计算了一次' ); for (let i = 1 ; i <= target; i++) { sum += i; } memoSum = sum; return sum; }
1 2 3 4 5 6 7 8 9 10 11 import { memoSummation } from './memoFn' ;memoSummation (10 );memoSummation (50 );memoSummation (100 );memoSummation (50 );memoSummation (100 );memoSummation (100 );memoSummation (100 );memoSummation (100 );memoSummation (100 );
'我出现,表示又重新计算了一次'
只打印了 5 次。
将memoFn.jsx
模块用伪代码表示,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const memoFn = (function ( ) { let prevTarget = -1 ; let memoSum = 0 ; return function memoFn (target ) { if (prevTarget > 0 && prevTarget === target) { return memoSum; } prevTarget = target; let sum = 0 ; console .log ('我出现,表示又重新计算了一次' ); for (let i = 1 ; i <= target; i++) { sum += i; } memoSum = sum; return sum; }; })(); const memoSummation = memoFn;
hooks 中的记忆函数 hooks 提供的 api 中,大多都有记忆功能,例如:
useState
useEffect
useCallback
useMemo
useLayoutEffect
useReducer
useRef
这里主要讲useCallback
和useMemo
。
useMemo useMemo 缓存计算结果 。它接收两个参数,第一个参数是回调函数(返回计算结果),第二个参数是依赖项(数组),当依赖项中某一项发生变化时,结果将会重新计算。const memorizedValue = useMemo(() => {}, deps)
useCallback useCallback 缓存函数。 他的使用跟useMemo
几乎一样,当依赖项中的某一个发生变化时,返回一个新函数。const memorizedFn = useCallback(() => {}, deps)
伪代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 let memoizedState = [];let hookIndex = 0 ;function useCallbacks (callback, nextDeps ) { const prevState = memoizedState[hookIndex]; if (prevState) { let [prevCallback, prevDeps] = prevState; let same = nextDeps.every ((item, index ) => item === prevDeps[index]); if (same) { console .log ('返回旧函数' ); return prevCallback; } } memoizedState[hookIndex] = [callback, nextDeps]; console .log ('返回新函数' ); return callback; } export default useCallbacks;
useCallback 和 useMemo 的区别
useCallback 缓存的是callback
函数本身,useMemo 缓存的是callback
函数的计算结果,也可以是一个函数
都是利用闭包缓存计算结果。只有在函数或计算的过程非常复杂时才考虑使用
useCallback/useMemo 可以用来优化子组件和当前组件。优化子组件时,可以防止子组件没必要的重复渲染,useCallback 需要配合 React.memo 一起使用,useMemo 不需要;优化当前组件时,useCallback 主要用于某个会多次 re-render 组件中的没有依赖项的函数,useMome 主要用于缓存复杂的计算逻辑。
useCallback(fn, deps) 相等于 useMemo(() => fn, deps)
是否需要优化 通过记忆函数的原理,我们应该知道,创建记忆函数并不是没有代价的,我们需要创建闭包,占用更多的内存,用以解决计算上的冗余 。
对于一个函数组件来说,我们在内部会创建许多函数,是否都有必要使用 useCallback 呢?思考下面代码
useCallback 是否有必要包裹 setCount 函数?
1 2 3 const memoAdd = useCallback (() => { setCount (count++); }, [count]);
存在依赖项时 ,如果 memoFn 不作为 props 传给子组件,仅做一个缓存函数的作用,这样是不是也有达到优化效果?
答案是没必要 。创建函数的消耗很小,依赖项的对比反而有一定的性能开销。
当一个函数执行完毕,就会从函数调用栈被弹出,里面的内存也会被回收。对于函数组件来说也一样,当内部函数执行完毕后也会被释放掉。所以函数式组件的性能是非常快的。 而当我们使用 useCallback 时,由于新增了对闭包的使用,新增了依赖项的对比逻辑,如果盲目使用他们,可能会让组件变得更慢。大多数情况下你不需要使用 useCallback/useMemo。
那么,什么时候使用 useCallback 比较合适呢?
适用场景 当函数或计算的过程非常复杂时 ,才优先考虑使用useCallback/useMemo
。
当函数(或子组件)非常复杂时 默认情况下,父组件的重新渲染会导致子组件也重新渲染。如果子组件的 props 未发生变化时,子组件就没有重新渲染的必要。
所以当函数非常复杂时,我们可以将 useCallbak 的返回值”缓存函数”传给子组件,然后配合子组件使用 React.memo 解决子组件不必要的渲染问题。
案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import React , { useState, useCallback, useMemo } from 'react' ;import Button from './Button' ;export default function UseCallback ( ) { const [count1, setCount1] = useState (0 ); const [count2, setCount2] = useState (0 ); const [count3, setCount3] = useState (0 ); const handleClickButton1 = ( ) => { setCount1 (count1 + 1 ); }; const handleClickButton2 = useCallback (() => { setCount2 (count2 + 1 ); }, [count2]); return ( <div > <h1 > 未使用useCallback</h1 > <div > <Button onClickButton ={handleClickButton1} > Button1</Button > </div > <div > <Button onClickButton ={handleClickButton2} > Button2</Button > <p > Button2 上的on函数使用了useCallback</p > </div > <div > <Button onClickButton ={() => { setCount3(count3 + 1); }} > Button3 </Button > </div > </div > ); }
子组件要配合 React.memo 一起使用。 Button 子组件,假设这个组件非常复杂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' ;const Button = ({ onClickButton, children, count } ) => { return ( <> <button onClick ={onClickButton} > {children}</button > <span > {Math.random()}</span > {count && <span > count: {count}</span > } </> ); }; export default React .memo (Button );
会多次re-render
的组件中,且函数没有任何依赖时 如果某组件一定会多次re-render
,且函数没有任何依赖,可以考虑使用 useCallback 降低多次执行带来的重复创建同样方法的负担。
1 2 3 4 5 <input type="text" onChange={inputChange} />; const inputChange = useCallback ((e ) => { setValue (e.target .value ); }, []);
同样的场景,如果函数只会渲染一次,那么使用 useCallback 就完全没必要。
闭包带来的问题 通过前面的例子可以知道,handleClickButton2
缓存函数仅在 count
不变时保持稳定。 如果想要保持 handleClickButton2
引用一直稳定 ,要把依赖项移除,用空数组作为参数,这会导致访问到的 count
总是初始值 ,逻辑上引发了更大的问题,也就是闭包问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React , { useState, useCallback, useMemo } from 'react' ;import Button from './Button' ;export default function UseCallback ( ) { const [count4, setCount4] = useState (0 ); const stableClickFn = useCallback (() => { console .log (count4); setCount4 (count4 + 1 ); }, []); return ( <div > <h1 > 缓存函数引用保持稳定时状态不更新问题</h1 > <Button onClickButton ={stableClickFn} > stable button4</Button > <div > count 只会更新一次</div > <div > count4: {count4}</div > </div > ); }
想要解决这个问题,可以看hooks 闭包陷阱 中的解决办法。
react 团队为了解决这个问题,准备引入useEvent
,它具备以下属性:
每次重新渲染时都不会重新创建该函数
该函数将可以访问 props/state 的最新值
总结 useCallback/useMemo
的作用在于利用记忆函数减少无效的re-render
,来达到性能优化的作用。记忆函数的原理,是创建闭包,但创建闭包是有代价的,会占用更多的内存,用以解决计算上的冗余。所以我们需要根据场景来权衡是否有必要使用useCallback/useMemo
。
参考:https://zhuanlan.zhihu.com/p/56975681 https://mp.weixin.qq.com/s/MnkycB8D9kCRrsXaLBfgXg