React的备忘录API可以用来优化你的React函数组件的渲染行为。我们将先通过一个组件的例子来说明这个问题,然后用React的备忘录API来解决这个问题。
请记住,React中的大部分性能优化都是不成熟的。React的默认速度很快,所以每一个性能优化都是为了防止某些东西开始觉得慢而选择的。
注意:如果你的React组件仍然在用React memo渲染,请查看这个关于React的useCallback Hook的指南。通常情况下,重新渲染是与回调处理程序相关的,而回调处理程序在每次渲染时都会改变。
注意:不要把React的备忘录API和React的useMemo Hook搞错。React memo是用来包装React组件以防止重新渲染的,而useMemo是用来记忆值的。
让我们来看看下面这个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 }));
};
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleAddUser}>
Add User
</button>
<List list={users} />
</div>
);
};
const List = ({ list }) => {
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
};
const ListItem = ({ item }) => {
return <li>{item.name}</li>;
};
export default App;
如果你在App、List和ListItem组件的函数体中包含一个console.log 语句,你会看到每次有人在输入框中输入时,这些日志语句就会运行:
const App = () => {
console.log('Render: App');
...
};
const List = ({ list }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
};
const ListItem = ({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
};
在输入字段后,所有的组件都会重新渲染,因为App组件更新了它的状态,它所有的子组件都会默认重新渲染:
// after typing one character into the input field
Render: App
Render: List
Render: ListItem
Render: ListItem
这是React给出的默认行为,大多数时候,只要你的应用程序不开始感到缓慢,保持这种方式就可以了。
但是一旦它开始感觉很慢,比如每次用户在输入框中输入时都要渲染一个巨大的项目列表,你可以使用React的memo API来记忆你的组件的功能:
const List = React.memo(({ list }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
});
const ListItem = ({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
};
现在,当我们在输入框中输入时,只有App组件会重新渲染,因为它是唯一受状态改变影响的组件。List组件收到了之前的备忘道具,这些道具并没有改变,因此根本就没有重新渲染。ListItem紧随其后,没有使用React的备忘API,因为List组件已经阻止了重新渲染:
// after typing one character into the input field
Render: App
这就是React的memo功能,简而言之。看起来我们不需要对ListItem组件进行备忘。然而,一旦你用按钮向列表添加一个新的项目,在目前的实现下,你会看到以下输出:
// after adding an item to the list
Render: App
Render: List
Render: ListItem
Render: ListItem
Render: ListItem
通过向列表中添加一个项目,列表发生变化,从而导致List组件的更新。目前,这是我们所期望的行为,因为我们想渲染所有的项目(2个)和新的项目(1个)。但也许只渲染一个新项目而不是所有项目会更有效:
const List = React.memo(({ list }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
});
const ListItem = React.memo(({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
});
在尝试了之前的方案后,通过向列表中添加一个项目,用React的备忘录函数的新实现,你应该看到以下输出:
// after adding an item to the list
Render: App
Render: List
Render: ListItem
只有新的项目显示出来了。列表中所有先前的项目保持不变,因此不会重新渲染。现在,只有那些受到状态变化影响的组件才会重新渲染。
你可能想知道为什么你不在所有的组件上使用React memo,或者为什么React memo不是所有React组件的默认配置。
在内部,React的memo函数必须比较之前的道具和新的道具,以决定是否应该重新渲染组件。通常情况下,这种比较的计算可能比重新渲染组件更昂贵。
总之,当你的React组件变得很慢,你想提高它们的性能时,React的备忘录功能就会大放异彩。这种情况通常发生在数据量大的组件中,比如巨大的列表,一旦有一个数据点发生变化,很多组件就必须重新渲染。