我博客的一位读者在Facebook上向我提出了一个有趣的问题。他说他的队友们,无论在什么情况下,都是把每个回调函数包裹在useCallback() 。
import React, { useCallback } from 'react';
function MyComponent() {
const handleClick = useCallback(() => {
// handle the click event
}, []);
return <MyChild onClick={handleClick} />;
}
"每个回调函数都应该被备忘录化,以防止对使用该回调函数的子组件进行无用的重新渲染",这是他的队友们的推论。
这种推理与事实相去甚远。这种没有经过分析的useCallback() ,会使组件的速度变慢。
在这篇文章中,我将解释如何正确使用useCallback() 。
1.了解函数的平等性检查
在深入了解useCallback() 的用法之前,让我们先区分一下useCallback() 所解决的问题--函数平等检查。
JavaScript中的函数是一等公民,也就是说,一个函数是一个普通的对象。函数对象可以被其他函数返回,可以被比较,等等:任何你能对一个对象做的事情。
让我们写一个函数factory() ,它可以返回对数字求和的函数。
function factory() {
return (a, b) => a + b;
}
const sum1 = factory();
const sum2 = factory();
sum1(1, 2); // => 3
sum2(1, 2); // => 3
sum1 === sum2; // => false
sum1 === sum1; // => true
函数sum1 和sum2 共享相同的代码源,但它们是不同的函数对象。比较它们sum1 === sum2 ,评估为false 。
这就是JavaScript对象的工作方式。一个对象(包括一个函数对象)只等于它自己。
2.useCallback()的目的
共享相同代码的不同函数对象经常在React组件内部创建。
function MyComponent() {
// handleClick is re-created on each render
const handleClick = () => {
console.log('Clicked!');
};
// ...
}
handleClick 是在每次渲染 ,MyComponent是一个不同的函数对象。
因为内联函数很便宜,所以在每次渲染上重新创建函数并不是什么问题。每个组件有几个内联函数是可以接受的。
但在某些情况下,你需要在不同的渲染中保持一个单一的函数实例:
- 一个被包裹在
React.memo()内的功能组件接受一个函数对象道具。 - 当函数对象是对其他钩子的依赖时,例如
useEffect(..., [callback]) - 当函数有一些内部状态时,例如,当函数被放行或被节流时。
这时useCallback(callbackFun, deps) 有所帮助:给定相同的依赖值deps ,钩子在渲染之间返回相同的函数实例(又称记忆化)。
import { useCallback } from 'react';
function MyComponent() {
// handleClick is the same function object
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []);
// ...
}
handleClick 在 MyComponent渲染之间,变量总是有相同的回调函数对象。
3.一个好的用例
想象一下,你有一个组件可以渲染一个大的项目列表:
import useSearch from './fetch-items';
function MyBigList({ term, onItemClick }) {
const items = useSearch(term);
const map = item => <div onClick={onItemClick}>{item}</div>;
return <div>{items.map(map)}</div>;
}
export default React.memo(MyBigList);
这个列表可能很大,可能有几百个项目。为了防止无用的列表重新渲染,你把它包装成React.memo() 。
MyBigList 的父组件提供了一个处理函数,以了解一个项目被点击的情况。
import { useCallback } from 'react';
export function MyParent({ term }) {
const onItemClick = useCallback(event => {
console.log('You clicked ', event.currentTarget);
}, [term]);
return (
<MyBigList
term={term}
onItemClick={onItemClick}
/>
);
}
当MyParent 组件重新渲染时,onItemClick 函数对象保持不变,不会破坏MyBigList 的记忆化。
这就是useCallback() 的一个很好的用例。
4.一个坏的用例
让我们看看另一个例子。
import { useCallback } from 'react';
function MyComponent() {
// Contrived use of `useCallback()`
const handleClick = useCallback(() => {
// handle the click event
}, []);
return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
return <button onClick={onClick}>I am a child</button>;
}
应用useCallback() 有意义吗?很可能不是,因为<MyChild> 组件很轻,它的重新渲染不会产生性能问题。
不要忘了,每次MyComponent 渲染时都会调用useCallback() 钩子。即使是useCallback() 返回相同的函数对象,在每次重新渲染时仍然要重新创建内联函数(useCallback() 只是跳过它)。
通过使用useCallback() ,你也增加了代码的复杂性。你必须使useCallback(..., deps) 的deps 与你在memoized回调中使用的内容保持同步。
总而言之,优化的代价比没有优化的代价要大。
只要接受渲染会产生新的函数对象。
import { useCallback } from 'react';
function MyComponent() {
const handleClick = () => {
// handle the click event
};
return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
return <button onClick={onClick}>I am a child</button>;
}
5.总结
在考虑性能调整的时候,请记住这句话。
在优化之前先进行剖析
当决定使用一种优化技术时,包括记忆化,特别是useCallback() ,要做到:
- 首先 -剖析
- 然后量化所增加的性能(例如:
150ms与50ms渲染速度的增加)。
然后问自己:与增加的复杂性相比,增加的性能是否值得使用useCallback() ?