为什么会有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 组件,这种变化其实是不需要渲染子组件的,结果发现造成 List
和 ListItem
的重新渲染,造成性能上的浪费。
切记:不要过早的进行性能优化
我们使用 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
| Render: App Render: List Render: ListItem Render: ListItem
|
从上面运行的结果看,每次输入一个字符都会引起所有组件的渲染。 究竟是哪里出了问题呢?
我们回顾一下 List
组件是如何渲染的:
1 2 3 4 5 6 7
| const App = () => { return ( <List list={users} onRemove={handleRemove} /> ) }
|
List
组件接收两个 props
: users
和 handleRemove
。 users
只有点击按钮的时候才会发生变化,所以 handleRemove
才是导致重复渲染的原因!
每次 App
由于键盘输入重新渲染的时候,handleRemove
会被重新定义,导致进行dom diff的时候发现 handleRemove
前后不一致重新渲染。
终极解决方案
使用 useCallback 来缓存回调函数:
1 2 3 4 5 6 7 8 9 10
| const App = () => { ... const handleRemove = React.useCallback( (id) => setUsers(users.filter((user) => user.id !== id)), [users] ); ... };
|
第二个参数是数组代表依赖项,之后依赖项发生变化才会返回新的函数。
useCallback总结
useCallback
是用来缓存函数的。当函数被传递给其他组件时,无需担心在父组件的每次重新渲染时都重新初始化函数,这是一个很小的性能提高了。很多时候结合 memo API
一起使用时,useCallback
才能发挥出作用!
全文完。