结论
先说结论useCallback
和useMemo
都可缓存函数的引用或值,但是从更细的使用角度来说useCallback
缓存函数的引用,useMemo
缓存计算数据的值。
回顾
useCallback回顾
const memoizedCallback \= useCallback(
() \=> {
doSomething(a, b);
},
\[a, b\],
);
根据官网文档的介绍我们可理解:在a
和b
的变量值不变的情况下,memoizedCallback
的引用不变。即:useCallback
的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
useMemo回顾
const memoizedValue \= useMemo(() \=> computeExpensiveValue(a, b), \[a, b\]);
根据官方文档的介绍我们可理解:在a
和b
的变量值不变的情况下,memoizedValue
的值不变。即:useMemo
函数的第一个入参函数不会被执行,从而达到节省计算量的目的。
分析
useCallback分析
我做了这样一个简单示例,我们来分析一下现象。
// 在Hooks中获取上一次指定的props
const usePrevProps \= value \=> {
const ref \= React.useRef();
React.useEffect(() \=> {
ref.current \= value;
});
return ref.current;
}
function App() {
const \[count, setCount\] \= React.useState(0);
const \[total, setTotal\] \= React.useState(0);
const handleCount \= () \=> setCount(count + 1);
const handleTotal \= () \=> setTotal(total + 1);
const prevHandleCount \= usePrevProps(handleCount);
console.log('两次处理函数是否相等:', prevHandleCount \=== handleCount);
return (
<div\>
<div\>Count is {count}</div\>
<div\>Total is {total}</div\>
<br/\>
<div\>
<button onClick\={handleCount}\>Increment Count</button\>
<button onClick\={handleTotal}\>Increment Total</button\>
</div\>
</div\>
)
}
ReactDOM.render(<App /\>, document.body)
我们重点看这一行
const handleCount = () => setCount(count + 1);
根据我们之前的理解,我们知道每次App组件渲染时这个handleCount
都是重新创建的一个新函数。
const prevHandleCount \= usePrevProps(handleCount);
console.log('两次处理函数是否相等:', prevHandleCount \=== handleCount);
我们也可以通过比较上一次的prevHandleCount
和本次的handleCount
。可以明确的知道每次渲染时handleCount
都是重新创建的一个新函数。
问题:它有什么问题呢?当我们将handleCount
作为props传递给其他组件时会导致像PureComponent
、shouldComponentUpdate
、React.memo
等相关优化失效(因为每次都是不同的函数)
展示问题Gif:
为了解决上述的问题,我们需要引入useCallback
,通过使用它的依赖缓存功能,在合适的时候将handleCount
缓存起来。我创建了一个简单示例,来看看是如何解决的吧。
// 在Hooks中获取上一次指定的props
const usePrevProps \= value \=> {
const ref \= React.useRef();
React.useEffect(() \=> {
ref.current \= value;
});
return ref.current;
}
function App() {
const \[count, setCount\] \= React.useState(0);
const \[total, setTotal\] \= React.useState(0);
const handleCount \= React.useCallback(() \=> setCount(count \=> count + 1), \[\]);
const handleTotal \= () \=> setTotal(total + 1);
const prevHandleCount \= usePrevProps(handleCount);
console.log('两次处理函数是否相等:', prevHandleCount \=== handleCount);
return (
<div\>
<div\>Count is {count}</div\>
<div\>Total is {total}</div\>
<br/\>
<div\>
<button onClick\={handleCount}\>Increment Count</button\>
<button onClick\={handleTotal}\>Increment Total</button\>
</div\>
<AotherComponent onClick\={handleCount} /\>
</div\>
)
}
const AotherComponent \= React.memo(function AotherComponent({ onClick }) {
console.log('AotherComponent 组件渲染');
return (
<button onClick\={onClick}\>AotherComponent - Inrement Count</button\>
)
})
ReactDOM.render(<App /\>, document.body)
这次我们重点看这行
const handleCount \= React.useCallback(() \=> setCount(count \=> count + 1), \[\]);
我使用useCallback
来缓存了函数,依赖项(deps)是一个空数组它代表这个函数在组件的生成周期内会永久缓存
。
const AotherComponent \= React.memo(function AotherComponent({ onClick }) {
console.log('AotherComponent 组件渲染');
return (
<button onClick\={onClick}\>AotherComponent - Inrement Count</button\>
)
})
因为我们的handleCount
是一个缓存函数,所以当我们传递给经过React.memo
优化的组件AotherComponent
时不会触发渲染
温馨提示:在选择useCallback
的依赖(deps)时请经过仔细的考虑,比如下面这样的依赖是达不到最好的优化效果,因为当我们增加了一次count
时,handleCount
的引用就会更改。
解决问题Gif,我们可以看到prevHandleCount等于handleCount,也没有多余的渲染:
useMemo分析
useMemo
和useCallback
几乎是99%像是,当我们理解了useCallback后理解useMemo就非常简单。
他们的唯一区别就是:useCallback
是根据依赖(deps)缓存第一个入参的(callback)。useMemo
是根据依赖(deps)缓存第一个入参(callback)执行后的值。
你明白了吗? 如果还没明白我贴一下useCallback
和useMemo
的源码你来看看区别。
// 注:为了方便理解我省去了一些flow语法
function updateCallback(callback, deps) {
const hook \= updateWorkInProgressHook();
const nextDeps \= deps \=== undefined ? null : deps;
const prevState \= hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps \= prevState\[1\];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState\[0\];
}
}
}
hook.memoizedState \= \[callback, nextDeps\];
return callback;
}
function updateMemo(nextCreate, deps) {
const hook \= updateWorkInProgressHook();
const nextDeps \= deps \=== undefined ? null : deps;
const prevState \= hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps \= prevState\[1\];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState\[0\];
}
}
}
const nextValue \= nextCreate(); // 🤩
hook.memoizedState \= \[nextValue, nextDeps\];
return nextValue;
}
聪明的你一定看出来区别是啥了对吧。
useMemo
一般用于密集型计算大的一些缓存。
下面我写了一个简单示例,来展示useMemo
如何使用的。
// 在Hooks中获取上一次指定的props
const usePrevProps \= value \=> {
const ref \= React.useRef();
React.useEffect(() \=> {
ref.current \= value;
});
return ref.current;
}
function App() {
const \[count, setCount\] \= React.useState(0);
const \[total, setTotal\] \= React.useState(0);
const calcValue \= React.useMemo(() \=> {
return Array(100000).fill('').map(v \=> /\*一些大量计算\*/ v);
}, \[count\]);
const handleCount \= () \=> setCount(count \=> count + 1);
const handleTotal \= () \=> setTotal(total + 1);
const prevCalcValue \= usePrevProps(calcValue);
console.log('两次计算结果是否相等:', prevCalcValue \=== calcValue);
return (
<div\>
<div\>Count is {count}</div\>
<div\>Total is {total}</div\>
<br/\>
<div\>
<button onClick\={handleCount}\>Increment Count</button\>
<button onClick\={handleTotal}\>Increment Total</button\>
</div\>
</div\>
)
}
ReactDOM.render(<App /\>, document.body)
这次我们重点看这行,只有当count
变量值改变的时候才会执行useMemo
第一个入参的函数。
const calcValue \= React.useMemo(() \=> {
return Array(100000).fill('').map(v \=> /\*一些大量计算\*/ v);
}, \[count\]);
通过useMemo
的依赖我们就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。
总结
我们一起回顾了useCallback
和useMemo
的基本使用方法,接着找到了影响性能的根本原因然后通过useCallback
如何去解决性能问题。最后我们学习了如何使用useMemo
去缓存了计算量密集的函数。我们还通过观察React Hooks源码观察了useCallback
和useMemo
最根本的区别,这让我们在开发时可以做出正确的选择。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。