大家都知道useCallback
是用来做函数缓存的,但是估计很多人都不知道它真正的用法。
useCallBack 怎么使用呢 |
文章主要说明的两点
useCallBack
不是每个函数都需要使用!useCallBack
在什么情况下使用?
看到这里,有些笔友就要发问三连了:
-
为什么不用
useCallBack
把每个函数都包一下呢? -
useCallBack
不是缓存工具吗? -
将每个函数都缓存不是可以更好提升性能吗?
useCallBack
是一个缓存工具没错。但实际上他并不能阻止函数都重现构建。
举个例子
//Com组件
const Com = () => {
//示例1包裹了useCallBack的函数
const fun1 = useCallBack(() => {
console.log('示例一函数');
...
},[])
//示例2没有包裹useCallBack的函数
const fun2 = () => {
console.log('示例二函数');
...
}
return <div></div>
}
大家看上方这种结构的组件,Com
组件中包含了fun1
和fun2
两个函数。
是不是认为当Com
组件重新渲染的时候,只有fun2
(没有使用useCallBack
的函数)函数会被重新构建,而fun1
(使用了useCallBack
的函数)函数不会被重新构建?
实际上,被useCallBack
包裹了的函数也会被重新构建并当成useCallBack
函数的实参传入。
工作原理 |
useCallBack
的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack
都无法阻止组件render时函数的重新创建!!
性能 |
每一个被useCallBack
的函数都将被加入useCallBack
内部的管理队列。而当我们大量使用useCallBack
的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack
的组件重新渲染的时候都需要去遍历useCallBack
内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。
在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack
不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。
useCallBack 在什么情况下使用? |
在往子组件传入了一个函数并且子组件被React.momo
缓存了的时候使用
像上一节所说的,useCallBack
的作用不是阻止函数创建,而是在依赖不变的情况下返回旧函数地址(保持地址不变)。
React.memo() |
React.memo()
,是一种缓存技术。能看到这里的笔友我想都不需要我详细解释React.memo
是干什么的。
简单说,React.memo()
是通过校验props
中的数据是否改变的来决定组件是否需要重新渲染的一种缓存技术,具体点说React.memo()
其实是通过校验Props
中的数据的内存地址是否改变来决定组件是否重新渲染组件的一种技术。
示例 |
假设我们往子组件(假设子组件为Child组件)传入一个函数呢?当父组件的其他State(与Child组件无关的state)改变的时候。那么,因为状态的改变,父组件需要重新渲染,那被React.memo
保护的子组件(Child组件)是否会被重新构建?
就这个问题,举个栗子:
代码示例一
import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
const [parentState,setParentState] = useState(0); //父组件的state
//需要传入子组件的函数
const toChildFun = () => {
console.log("需要传入子组件的函数");
...
}
return (<div>
<Button onClick={() => setParentState(val => val+1)}>
点击我改变父组件中与Child组件无关的state
</Button>
//将父组件的函数传入子组件
<Child fun={toChildFun}></Child>
<div>)
}
/**被memo保护的子组件**/
const Child = memo(() => {
consolo.log("我被打印了就说明子组件重新构建了")
return <div><div>
})
当我点击父组件中的Button
改变父组件中的state
,子组件会不会重新渲染?乍一看,改变的是parentState
这个变量,和子组件半毛钱关系没有,子组件还被React.memo
保护着,好像是不会被重新渲染。但这里的问题是,你要传个其他变量进去这也就走的通了,但是传入的是函数,会重新渲染。
React.memo
检测的是props
中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props
检测到栈地址更新。也就引发了子组件的重新渲染。
所以,在上面的代码示例里面,子组件是要被重新渲染的。
正确示例 |
那么如何才能让子组件不进行重新渲染呢?useCallBack
的正确使用方法来了。
使用useCallBack
包一下需要传入子组件的那个函数。那样的话,父组件重新渲染,子组件中的函数就会因为被useCallBack
保护而返回旧的函数地址,子组件就不会检测成地址变化,也就不会重选渲染。
import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
const [parentState,setParentState] = useState(0); //父组件的state
//需要传入子组件的函数
//只有这里和上一个示例不一样!!
//只有这里和上一个示例不一样!!
//只有这里和上一个示例不一样!!
//只有这里和上一个示例不一样!!
//只有这里和上一个示例不一样!!
//只有这里和上一个示例不一样!!
const toChildFun = useCallBack(() => {
console.log("需要传入子组件的函数");
...
},[])
return (<div>
<Button onClick={() => setParentState(val => val+1)}>
点击我改变父组件中与Child组件无关的state
</Button>
//将父组件的函数传入子组件
<Child fun={toChildFun}></Child>
<div>)
}
/**被memo保护的子组件**/
const Child = memo(() => {
consolo.log("我被打印了就说明子组件重新构建了")
return <div><div>
})
这样,子组件就不会被重新渲染了。
代码示例一和代码示例二中的区别只有被传入的子组件的函数(toChildFun
函数)是否被useCallBack
保护。
我们只需要使用useCallBack
保护一下父组件中传入子组件的那个函数(toChildFun
函数)保证它不会在没有必要的情况下返回一个新的内存地址就好了。
总结 |
useCallBack
不要每个函数都包一下,否则就会变成反向优化,useCallBack
本身就是需要一定性能的
useCallBack
并不能阻止函数重新创建,它只能通过依赖决定返回新的函数还是旧的函数,从而在依赖不变的情况下保证函数地址不变
useCallBack
需要配合React.memo
使用
有useCallback
包裹的函数会在更新时候有两个函数(旧的和新的)。这两个函数会去比对依赖,来决定返回哪个。比对失败的那个会被销毁。