前言
我们在得知useCallback能够优化性能后,恨不得每个函数都要拿useCallback包裹一下;不过我们需要明白: 错误或盲目的使用useCallback会导致性能不升反降;
useCallback这种memoized函数也是需要成本的,比如增加了额外的deps变化判断,再比如可能会获得更多的内存分配…; 因此性能优化带来的好处可能抵消不了它的成本; 打个比方就像你开车
去10公里以外的小镇 和 你开车
去隔壁的邻居家);
那么,什么情况下才可以使用useCallback去进行性能优化呢?
在你觉得这种性能的交换比较合理的时候.常用的场景比如:
函数 functionA 在 useEffect 内部被调用,为了避免 useEffect 的频繁触发,所以我用useCallback将函数 functionA 包起来;
当函数 functionD 被传递给子组件,为了避免子组件进行不必要的频繁渲染,所以我用useCallback将函数functionD 包起来[※]
正文
场景一
在某些场景里需要在useEffect里调用一个函数,出于某些原因.你无法把这个函数移动到effect内部;
const Example = ({ someProp }) => {
const doSomething = () => {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, [doSomething]); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
因为useEffect中调用的 doSomething
函数使用了 someProp
这个props,所以需要将doSomething
放在useEffect的依赖列表里;
而为了确保这个函数不会随着组件渲染而重新创建,我们需要用useCallback把doSomething
包裹起来,如下:
const Example = ({ someProp }) => {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const doSomething = useCallback(() => {
// ... Does something with someProp ...
}, [someProp]); // ✅ useCallback 的所有依赖都被指定了
useEffect(() => {
doSomething();
}, [doSomething]); // ✅ useEffect 的所有依赖都被指定了
}
场景二
类似于场景一, 在某些情况下,你需要在useCallback包裹的函数中调用另一个函数,记得要给另一个函数也包上useCallback
const Example = ({ prop1, prop2 }) => {
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const something = useCallback(() => {
// ... Does something with prop2 ...
}, [prop2]); // ✅ useCallback 的所有依赖都被指定了
// ✅ 用 useCallback 包裹以避免随渲染发生改变
const doSomething = useCallback(() => {
somethingOne();
// ... Does something with prop1 ...
}, [prop1, somethingOne]); // ✅ useCallback 的所有依赖都被指定了
useEffect(() => {
doSomething();
}, [doSomething]); // ✅ useEffect 的所有依赖都被指定了
}
场景三
当函数 functionD 被传递给子组件:
const ParentComp = () => {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const [ name, setName ] = useState('hi~')
const changeName = (newName) => setName(newName)
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
const ChildComp = ({ name, onClick }) => {
console.log('render child-comp ...')
return <>
<div>Child Comp ... {name}</div>
<button onClick={() => onClick('hello')}>改变 name 值</button>
</>
}
我们先来看一下组件触发re render
的时机
- 组件的state的属性改变时;
- 组件的props的任意属性值改变时;
- 父组件重新rerender时;
根据组件的从新渲染机制,我们再来重新看一下代码:
const ParentComp = () => {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const [ name, setName ] = useState('hi~')
const changeName = (newName) => setName(newName) // 父组件渲染时会创建一个新的函数
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
const ChildComp = ({ name, onClick }) => {
console.log('render child-comp ...')
return <>
<div>Child Comp ... {name}</div>
<button onClick={() => onClick('hello')}>改变 name 值</button>
</>
}
会发现,当我点击父组件的button来增加次数时,纵使子组件的state和props没有任何变化,子组件依然会rerender,这显然不是我们所期待的,原因我们也清楚,因为父组件重新rerender
(state变化,父组件rerender) 且 子组件的props的onClick属性发生了变化
(父组件rerender时,函数changeName重新生成);
我们可以通过React.memo结合useCallback来解决子组件不必要rerender的问题
- 通过React.memo包裹子组件来解决因
父组件重新rerender
导致子组件rerender的问题; - 通过useCallback包裹函数changeName来解决因
子组件的props的onClick属性发生了变化
导致子组件rerender的问题;
const ParentComp = () => {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const [ name, setName ] = useState('hi~')
const changeName = useCallback((newName) => setName(newName), []) // ✅ 通过useCallback将函数changeName包裹
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
const ChildComp = React.memo(({ name, onClick }) => { // ✅ 通过React.memo 高阶组件将子组件包裹
console.log('render child-comp ...')
return <>
<div>Child Comp ... {name}</div>
<button onClick={() => onClick('hello')}>改变 name 值</button>
</>
})
[注:] React.memo和useCallback缺一不可;