React 的 memo API 可用于优化 React 函数组件的渲染行为。我们将首先用一个样例组件来展示问题所在,然后使用 React memo API 来解决它。
请记得 React 中的大部分性能优化是不成熟的。React 默认情况下很快,所以当你开始感觉性能变慢时可以尝试做一些性能优化。
注意:不要把 React 的 memo API 和 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 组件更新了它的 state,默认情况下它的所有子组件会重新渲染。
// 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 组件重新渲染,因为它是受 state 变化影响的唯一的组件。List 组件接收的是之前记忆化的 props,而那些 props 没有发生变化,因此无需再次渲染。下层的 ListItem 没有使用 React memo API,因为 List 组件已经阻止了重新渲染。
// after typing one character into the input field
Render: App
这就是 React 的 memo 函数。看起来我们无需为 ListItem 组件调用 memo 函数。不过如果点击了按钮新增了一个用户到列表后,你会发现当前的代码输出如下:
// after adding an item to the list
Render: App
Render: List
Render: ListItem
Render: ListItem
Render: ListItem
新增一个用户到列表后,列表发生了变化,而这会更新 List 组件。这是符合预期的行为,因为我们想渲染已有的两个用户以及这个新增的用户。但是如果只渲染新增的用户会更高效一些。
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 memo 函数的新代码,你会看到如下输出:
// after adding an item to the list
Render: App
Render: List
Render: ListItem
只有新增的用户渲染了。所有之前的用户保持原样,因此无需重新渲染。只有受到 state 变更影响的组件才会重新渲染。
你可能不禁会问为什么不为所有的组件使用 memo 函数,或者为什么 React 不为所有的组件默认用上 memo。
React memo 函数在其内部需要对比之前的 props 和新的 props,用来决定是否应该重新渲染该组件。通常这个对比的计算过程比重新渲染组件的开销要更大。
综上所述,React memo 函数在你的应用变得卡顿想要优化性能时特别有用。通常这会发生在含有大量数据的组件,例如一条数据变化后许多组件必须重新渲染海量的数据列表。