记忆函数
在开发实践中,有一种优化手段叫做记忆函数。
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