useCallback 和 useMemo 都是react可用于性能优化的内置hooks。
两者的区别在于:useCallback缓存的是一个函数,而useMemo缓存的是计算结果。
其使用语法如下:
// useCallback
// 第一个参数是一个回调函数,useCallback会缓存这个函数,返回缓存的回调函数
// 第二个参数是依赖项,只有当依赖项改变时,才会重新创建这个函数
const memorizedCallback = useCallback(()=>{
doSomething(a,b);
},[a,b])
// useMemo
// 第一个参数是一个函数,useMemo会缓存函数运行返回的值,返回缓存的值
// 第二个参数是依赖项,只有当依赖改变时,才会重新计算这个值
const memorizedValue = useMemo(()=>computeValue(a,b),[a,b])
在函数式组件中,每次UI的变化,都是通过重新执行整个函数来完成的,这和传统的类组件有很大区别:函数组件中并没有一个直接的方式在多次渲染之间维持一个状态。
在重新执行整个函数组件的过程中,其中的函数和引用类型的变量会创建新的(指向新的引用),导致函数组件在re-render前后,其中函数和引用类型变量是不相等的,这又会导致其他非必要的re-render。比如以下例子:
function Counter() {
const [count, setCount] = useState(0);
// 只要组件状态发生变化,每次都要创建一个新的事件处理函数
const handleIncrement = () => setCount(count + 1);
// ...
// 每次创建新函数的方式会让接收事件处理函数的组件需要重新渲染
return
当Counter组件因为其他数据(非count)发生变化而导致重新渲染的时候,重新执行整个Counter函数,会创建新的handleIncrement函数,而子组件Button会由于props-handleClick传入的handleIncrement函数改变而重新渲染,但其实这个渲染是不必要的,因为只有在count发生变化时,才应该导致Button组件的渲染。
此时,因为需要缓存的事handleIncrement函数,所以用useCallback优化一下。
// 需要做到:只有当count发生变化时,才需要重新定一个回调函数-useCallback
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count]
)
// 只有当依赖项count改变时,才会重新生成函数,不然都是返回的缓存的回调函数,不会触发Button子组件的重绘
// ...
return
3.1 useCallback
当子组件接收一个函数props时,一般会使用useCallback来缓存这个函数,减少不必要的re-render。以下例子:向子组件传递一个函数,在父组件每次re-render的时候,函数会重新创建新的,这会导致使用这个函数props的子组件也re-render,但这是不必要的,可以用useCallback来解决。
function ParentComponent() {
const onHandleClick = useCallback(() => {
// this will return the same function
// instance between re-renders
});
return (
);
}
3.2 useMemo
useMemo常用在以下两种场景的优化中:1)引用类型的变量 2)需要大量时间执行的计算函数。
const UseMemoDemo = () => {
// 调用这个函数需要大量时间去计算
const slowFunction = (number) => {
console.log('calling slow function')
for (let i = 0; i <= 1000000000; i++) {
}
return number * 2
}
const [inputNumber, setInputNumber] = useState(1)
const [dark, setDark] = useState(true)
// 场景1:执行某函数需要大量时间,使用useMemo来优化,在不必要执行函数的时候不执行函数
const doubleNumber = useMemo(() => {
return slowFunction(inputNumber)
}, [inputNumber])
// 场景2:每次组件更新会重新执行,内部的引用类型变量会重新创建,这会导致使用到引用类型变量的组件重新渲染,使用useMemo来让每次的变量相同
const themeStyle = useMemo(() => {
return {
background: dark ? 'black' : 'white',
color: dark ? 'white' : 'black'
}
}, [dark])
useEffect(() => {
console.log('themeStyle changed')
}, [themeStyle])
const handleChange = (e) => {
setInputNumber(parseInt(e.target.value))
}
return (
<>
{doubleNumber}
>
)
}
export default UseMemoDemo;
在使用useMemo优化slowFunction这个耗时较长的函数之前,不管是改变input的值,使下方显示input值*2(这是实际需要运行slowFunction这个耗时函数的);还是点击button切换主题(这时不需要运行slowFunction这个耗时函数);都犹豫需要组件重新渲染,导致运行slowFunction这个函数,造成性能问题。点击切换主题时,也有明显的延迟。
在优化后,改变主题不再运行slowFunction函数,因为useMemo缓存了计算结果,只要inputNumber这个依赖项没有改变,就不会重新计算。优化后,点击切换主题不再有明显的延迟。
对于themeStyle引用类型变量的优化,也是相同的道理。当因为改变input值导致组件重新渲染时,实际上themeStyle变量是没有改变的,但由于要重新执行组件函数,所以创建了新的引用。这使得使用到引用类型变量的组件(button)重新渲染,造成性能浪费。
在使用useMemo优化后,themeStyle只会在变量dark的改变时改变,其他时候是useMemo缓存的值,不会因为其他无关变量改变或组件重绘而改变,造成不必要的组件re-render。
最后,更多的关于什么时候使用useCallback和useMemo需要在项目实践中花时间去思考性能优化的点。不能盲目地使用useCallback和useMemo,因为两者都需要内存去缓存,过多的非必要的使用也是不利于应用的性能的。