为什么会有useCallback?

运行以下demo,发现有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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;

为每个组件增加 console.log 日志,检查组件是否会重复渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 组件,这种变化其实是不需要渲染子组件的,结果发现造成 ListListItem 的重新渲染,造成性能上的浪费。

切记:不要过早的进行性能优化

我们使用 React.memo 来避免重复渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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>
);
});

当你以为问题被解决的时候,现实会给你重重一击:

1
2
3
4
5
6
// after typing one character into the input field

Render: App
Render: List
Render: ListItem
Render: ListItem

从上面运行的结果看,每次输入一个字符都会引起所有组件的渲染。 究竟是哪里出了问题呢?

我们回顾一下 List 组件是如何渲染的:

1
2
3
4
5
6
7
const App = () => {
// How we're rendering the List in the App component
return (
//...
<List list={users} onRemove={handleRemove} />
)
}

List 组件接收两个 propsusershandleRemoveusers 只有点击按钮的时候才会发生变化,所以 handleRemove 才是导致重复渲染的原因!

每次 App 由于键盘输入重新渲染的时候,handleRemove 会被重新定义,导致进行dom diff的时候发现 handleRemove 前后不一致重新渲染。

终极解决方案

使用 useCallback 来缓存回调函数:

1
2
3
4
5
6
7
8
9
10
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]
);

...
};

第二个参数是数组代表依赖项,之后依赖项发生变化才会返回新的函数。

useCallback总结

useCallback 是用来缓存函数的。当函数被传递给其他组件时,无需担心在父组件的每次重新渲染时都重新初始化函数,这是一个很小的性能提高了。很多时候结合 memo API 一起使用时,useCallback 才能发挥出作用!