React 的 useCallback Hook 可用于优化 React 函数组件的渲染行为。 我们先通过一个示例组件来说明问题,然后通过 React 的 useCallback Hook 来解决。
请记住,React 中的大多数性能优化都为时过早。 默认情况下,React 速度很快,因此每项性能优化都是可选的,以防某些事情开始变得缓慢。
注意:不要将 React 的 useCallback Hook 与 React 的 useMemo Hook 混淆。 useCallback 用于记忆函数,而 useMemo 用于记忆值。
注意:不要将 React 的 useCallback Hook 与 React 的memo API 混淆。 useCallback 用于记忆函数,而 React memo 用于包装 React 组件以防止重新渲染。
让我们看下面的 React 应用程序示例,该应用程序呈现用户项目列表,并允许我们使用回调处理程序添加和删除项目。 我们使用 React 的 useState Hook 使列表有状态:
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
const App = () => {
const [users, setUsers] = React.useState([
{ id: 'a', name: 'Robin' },
{ id: 'b', name: 'Dennis' },
]);
const [text, setText] = React.useState('');
const handleText = (event) => {
setText(event.target.value);
};
const handleAddUser = () =>{
setUsers(users.concat({ id: uuidv4(), name: text }));
};
const handleRemove = (id) => {
setUsers(users.filter((user) => user.id !== id));
};
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleAddUser}>
Add User
</button>
<List list={users} onRemove={handleRemove} />
</div>
);
};
const List = ({ list, onRemove }) => {
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} onRemove={onRemove} />
))}
</ul>
);
};
const ListItem = ({ item, onRemove }) => {
return (
<li>
{item.name}
<button type="button" onClick={() => onRemove(item.id)}>
Remove
</button>
</li>
);
};
export default App;
使用我们学到的关于 React memo 的知识(如果你不知道 React memo,请先阅读指南然后再回来),它具有与我们的示例类似的组件,我们希望防止每个组件在用户输入时重新渲染。
const App = () => {
console.log('Render: App');
...
};
const List = ({ list, onRemove }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} onRemove={onRemove} />
))}
</ul>
);
};
const ListItem = ({ item, onRemove }) => {
console.log('Render: ListItem');
return (
<li>
{item.name}
<button type="button" onClick={() => onRemove(item.id)}>
Remove
</button>
</li>
);
};
在输入字段中键入以将项目添加到列表应该只会触发 App 组件的重新渲染,而不是其不关心此状态更改的子组件。 因此,React memo 将用于防止子组件更新:
const List = React.memo(({ list, onRemove }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} onRemove={onRemove} />
))}
</ul>
);
});
const ListItem = React.memo(({ item, onRemove }) => {
console.log('Render: ListItem');
return (
<li>
{item.name}
<button type="button" onClick={() => onRemove(item.id)}>
Remove
</button>
</li>
);
});
但是,也许令您惊讶的是,在输入字段中键入内容时,两个函数组件仍会重新呈现。 对于输入字段中输入的每个字符,您仍应看到与以前相同的输出:
// after typing one character into the input field
Render: App
Render: List
Render: ListItem
Render: ListItem
我们来看看传递给 List 组件的 props。
const App = () => {
// How we're rendering the List in the App component
return (
//...
<List list={users} onRemove={handleRemove} />
)
}
只要没有从列表 props 中添加或删除项目,即使在用户在输入字段中输入内容后 App 组件重新呈现,它也应该保持完整。 所以罪魁祸首是 onRemove 回调处理程序。
每当 App 组件在有人输入输入字段后重新呈现时,App 中的 handleRemove 处理函数都会被重新定义。
通过将这个新的回调处理程序作为道具传递给 List 组件,它会注意到与之前的渲染相比,道具发生了变化。 这就是为什么要重新渲染 List 和 ListItem 组件的原因。
最后,我们有了 React 的 useCallback Hook 的用例。 我们可以使用 useCallback 来记忆一个函数,这意味着这个函数只有在依赖数组中的任何依赖项发生变化时才会被重新定义:
const App = () => {
...
// Notice the dependency array passed as a second argument in useCallback
const handleRemove = React.useCallback(
(id) => setUsers(users.filter((user) => user.id !== id)),
[users]
);
...
};
如果用户状态因在列表中添加或删除项目而发生变化,则处理程序函数将被重新定义,并且子组件应重新呈现。
但是,如果有人只在输入字段中键入内容,则该函数不会重新定义并保持不变。因此,子组件不会收到更改的props,也不会在这种情况下重新渲染。
您可能想知道为什么不在所有函数上使用 React 的 useCallback Hook,或者为什么 React 的 useCallback Hook 不是所有函数的默认设置。
在内部,React 的 useCallback Hook 必须在每次重新渲染时比较依赖项数组中的依赖项,以决定是否应该重新定义函数。通常,这种比较的计算可能比重新定义函数更昂贵。
总之,React 的 useCallback Hook 用于记忆函数。当函数被传递给其他组件而不用担心每次重新渲染父组件时重新初始化函数时,这已经是一个很小的性能提升。然而,如您所见,当与 React 的memo API 一起使用时,React 的 useCallback Hook 开始发光。