useCallback同前面讲的useMemo、useLayoutEffect一样可以用来提高性能。
先来看看官网如何解释:
官网的解释很简单,其中有关键的两点:
- useCallback返回回调函数的memoized版本
- 相当于useMemo
也就是说useCallback得到的是传入它的回调函数,
下面来看看它解决的是什么样的场景:
import { useState, useCallback, memo } from "react";
import * as ReactDOM from "react-dom";
const Child = memo(function({val, onChange}) {
console.log('render...');
return <input value={val} onChange={onChange} />;
});
function App() {
const [val1, setVal1] = useState('');
const [val2, setVal2] = useState('');
// 每次父组件渲染,返回的是不同的函数引用
const onChange1 = evt => {
setVal1(evt.target.value);
};
const onChange2 = evt => {
setVal2(evt.target.value);
};
return (
<>
<Child val={val1} onChange={onChange1}/>
<Child val={val2} onChange={onChange2}/>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
我在其中一个Child中输入,却同时引起了另一个Child的渲染,分析产生这个问题的原因(以在第一个Child中输入为例):
-
val1
改变导致App
渲染 -
App
渲染,导致生成新的onChange1
以及onChange2
,传给Child
- 两个
Child
发现自己onChange
的引用变了,重新渲染
这种情况是很常见的,平时我们的代码基本上都存在这样的问题,一个父组件中必定会引用各种各样的子组件,父组件一旦渲染势必会引起它所有子组件也跟随渲染。但是如果onChange事件在这里不再是一个普通的函数而是一个耗费大量计算的函数,那么还是得注意到这方面的性能优化的。
这个问题该如何解决呢?
使用useCallback将onChange事件包裹起来即可:
// 每次父组件渲染,返回的是同一个函数引用
const onChange1 = useCallback( evt => {
setVal1(evt.target.value);
}, []);
const onChange2 = useCallback( evt => {
setVal2(evt.target.value);
}, []);
可以看到输入多少次打印多少次。
Child组件之间不再互相影响,原因就是依赖项为空数组,那么只在初始化的时候回调函数给到onChange1/onChange2变量,即使父组件App重新渲染,在useCallback的包装下给到Child的永远都是memoized版本,因此Child组件不会发生重新渲染。
另一个demo:debounce
debounce在项目中经常被使用,当我们从class组件切换到函数式组件的时候发现debounce的功能失效了,失效的原因通常是我们依照Lodash.debounce的官网所示用法简单的将需要防抖处理的函数放入debounce的回调函数,这样做在正常的运行环境中是没有问题的,但是,react组件在每次update后内部声明的函数就会重新创建,因此对这些函数的引用也就被改变,debounce回调函数也就每次被置入新的引用,debounce对其回调函数执行时机的控制也就失效了。而正好useCallback能够保留函数的memoized版本,也就使得debounce保留了update前对同一个函数的引用:
const [value, setInputValue] = React.useState("");
const [query, setSearchQuery] = React.useState("");
//在函数组件中用useCallback来确保不同生命周期内函数引用不变
const searchHandle = useCallback(val => {
console.log(val);
setSearchQuery(val);
}, [])
// 这里也要提前将debounce成品提出,不得放在handleChange
const debounceSearchQuery = useCallback(debounce(searchHandle, 300), []);
const handleChange = (e) => {
setInputValue(e.target.value);
debounceSearchQuery(e.target.value);
};
<input onChange={handleChange} placeholder="输入搜索内容" value={value} />
// 子组件:
<NewList query={query} />
其实在class组件中也是将debounce提前声明到constructor的做法来保留对回调函数的引用:
constructor(props) {
super(props);
this.inputChange = debounce(this.inputChange, 500);
}
与useEffect
前面官网讲到useCallback与useMemo相当,其实质都是返回一个memoized的结果,只是useCallback返回的是一个函数,useMemo返回的是一个值(简单数据类型或引用类型)。