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备忘录(如果你不知道React备忘录,请先阅读指南,然后再回来),它有与我们的例子类似的组件,我们要防止每一个组件在用户向输入栏输入时重新渲染。
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组件的道具。
const App = () => {
// How we're rendering the List in the App component
return (
//...
<List list={users} onRemove={handleRemove} />
)
}
只要没有从list 道具中添加或删除任何项目,即使用户在输入框中输入了什么,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]
);
...
};
如果users 状态发生变化,从列表中添加或删除一个项目,处理函数就会被重新定义,子组件应该重新渲染。
然而,如果有人只在输入框中输入,该函数就不会被重新定义,而是保持不变。因此,在这种情况下,子组件并没有收到改变的道具,也不会重新渲染。
你可能想知道为什么你不在所有的函数上使用React的useCallback Hook,或者为什么React的useCallback Hook首先不是所有函数的默认值。
在内部,React的useCallback Hook必须在每次重新渲染时比较依赖关系数组中的依赖关系,以决定是否应该重新定义该函数。通常情况下,这种比较的计算可能比重新定义函数更昂贵。
总之,React的useCallback Hook是用来备忘函数的。当函数被传递给其他组件时,已经是一个小小的性能提升,而不用担心函数在父组件的每次重新渲染中被重新初始化。然而,正如你所看到的,当React的useCallback Hook与React的memo API一起使用时,开始大放异彩。