基本使用
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本
该回调函数仅在某个依赖项改变时才会更新。
当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
下面我们以任务列表为例,来探究useCallback
import React from "react";
import { v4 as uuidv4 } from "uuid";
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>
);
};
const App = () => {
console.log("render 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>
);
};
export default App;
三个组件App、List、ListItem,删除的方法hangleRemove
每一次在input中输入值的时候,handleText触发状态变化,都会触发子组件re-render
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>
);
});
当我们使用React Memo的时候,发现,输入每个字母的时候还是会触发组件re-render,请确保对React Memo非常熟悉,参见 React Memo
为什么会不生效呢?
仔细看
App中每次输入字母,都会改变状态setText,App重新渲染就会触发handleRemove的re-defined
传递一个方法作为props给List组件,当props改变的时候就会re-render,这里是onRemove会在App重新渲染的时候re-defined。这就是为什么List和ListItem会重新渲染
我们之前说useCallback是优化函数的,意味着只有当某个依赖改变的时候,函数才会re-defined
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,只有Remove或者Add User改变users的时候,也就是useCallback的依赖users变化的时候,子组件才会重新渲染。
完整代码见 https://stackblitz.com/edit/react-9g4jp4
深入原理
和之前一样,调用 useCallback 也是追加到 Hook 链表上,不过这里着重强调了这个函数 f1 所指向的内存位置(随便画了一个),从而明确告诉我们:这个 *__f1* 始终是指向同一个函数。然后返回的 onClick 则是指向 Hook 中存储的 f1。
再来看看重渲染的情况:
重渲染的时候,再次调用 useCallback 同样返回给我们 f1 函数,并且这个函数还是指向同一块内存,从而使得 onClick 函数和上次渲染时真正做到了引用相等。
useCallback与useMemo
我们知道 useCallback 有个好基友叫 useMemo。还记得我们之前总结了 Memoization 的两大场景吗?
useCallback 主要是为了解决函数的”引用相等“问题,而 useMemo 则是一个”全能型选手“,能够同时胜任引用相等和节约计算的任务。
实际上,useMemo 的功能是 useCallback 的超集。与 useCallback 只能缓存函数相比,useMemo 可以缓存任何类型的值(当然也包括函数)。useMemo 的使用方法如下:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
其中第一个参数是一个函数,这个函数返回值的返回值(也就是上面 computeExpensiveValue 的结果)将返回给 memoizedValue 。因此以下两个钩子的使用是完全等价的:
useCallback(fn, deps);
useMemo(() => fn, deps);
使用场景
你可能会疑惑,既然useCallback可以达到性能优化,为什么不将React’s useCallbac作为默认的呢?
React’s useCallbac会计算依赖组的变化来决定是否re-define函数,通常计算本身和re-render比较更expensive
当方法函数会被传递给子组件,为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变;
- 子组件接收一个函数作为props
- 父组件状态变更,触发函数re-defined
- React’s useCallback 和 React’s memo 配合可以达到优化
函数被 useEffect 内部所使用,但为了避免频繁 useEffect 的频繁调用,用useCallback包一下
Reference
https://www.robinwieruch.de/react-usecallback-hook
https://mp.weixin.qq.com/s/vAeqveGywVlpkdOGp3Ublg
useCallback隐患