React.memo
如果你熟悉 React.PureComponent,那么就很好理解React.memo了。简单的来说两者共同点都对 Props 进行浅比较,区别是 PureComponent 用于 class 组件,而 memo 用于 function 组件。
特别说明本文的例子都不需要优化,只是帮助理解
来看个示例:
function App() {
const [num1, setNum1] = useState(0);
const [num2] = useState(0);
const handleAdd = () => {
setNum1(pre => pre + 1)
}
return (
<>
add
>
)
}
复制代码
function Num({ num, name }) {
console.log(${name}: ${num}
)
return (
export default Num;
复制代码
当用户点击按钮的时候,每次都会在控制台输出两个值。说明每次点击,两个都会重新渲染。
假设要是一个非常复杂的组件,达到每次重渲染都让页面感觉到卡顿了。那我们就需要想办法进行优化了。要怎么办呢?没错,就是用React.memo,只需要把包在React.memo里面就好。 如下例:
function Num({ num, name }) {
console.log(${name}: ${num}
)
return (
export default React.memo(Num);
复制代码
这时候再点击按钮,组件2就不再进行渲染。
上例中,memo比较了新旧Props(即我们从父组件传给的num和name)判断是否需要进行重渲染,如果比较的结果是一样的,就会重用上次的渲染结果。因为我们点击按钮只改变num1,num2一直不变,因此组件2没有再次渲染。
接下来,我们现在希望上面的例子在子组件可以进行减法,例子如下:
function App() {
…
const handleSub1 = () => {
setNum1(pre => pre - 1)
}
const handleSub2 = () => {
setNum2(pre => pre - 1)
}
return (
<>
add
>
)
}
复制代码
function Num({ num, name, handleSub }) {
console.log(${name}: ${num}
)
return (
export default React.memo(Num)
复制代码
这时候再去看下控制台,咦!怎么回事,memo失效了。这时候我们点击按钮,组件2也一直在重渲染。
这因为memo对父级传来的Props只是进行浅比较,而我们新加的handleSub是引用类型,父组件的state在变化一直在重渲染,handleSub也一直在重建,所以子组件即使使用了memo,也认为Props改变了。
这时候我们需要一个方法来解决这个问题,那就要用useCallBack。当然,如果传给子组件的是对象或者数组,那么我们将使用useMemo,后面讲到useMemo再细说。
引自(Hook API – React):当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,useCallBack将非常有用。
useCallBack
我们接着来看上面的例子,通过useCallBack进行优化,只需要将handleSub1和handleSub2用useCallback包裹起来。
function App() {
…
const handleSub1 = useCallback(() => {
setNum1(pre => pre - 1)
}, [])
const handleSub2 = useCallback(() => {
setNum2(pre => pre - 1)
}, [])
…
}
复制代码
因为useCallback包裹的函数依赖是空的所以它们就不会再重建了,这样传递给子组件的props就总是相同的handleSub了。
useMemo
useMemo有两个应用场景:
场景一:配合memo使用,保证父组件传递的引用类型props不重建,从而避免不必要的渲染。
还是借用上面的例子,如果给子组件传递数组时,不去保证数组不重建,那么会和之前一样,memo失效,所以同理,我们现在给组件包裹一层useMemo:
function App() {
…
const arr = useMemo(() => { return [] },[])
return (
<>
add
>
)
}
复制代码
function Num({ num, name, handleSub }) {
console.log(${name}: ${num}
)
return (
export default React.memo(Num)
复制代码
场景二:通常,我们不想组件重新渲染时一次又一次地计算一个计算昂贵的值时,我们可以使用 React.useMemo。
const Me = ({ girlFriendWords }) => {
// 假设girlFriendWords是一个字符串
const myReply = decideWhatToSay (girlFriendWords)
return {myReply}
}
复制代码
想象一下上面的代码场景:女朋友跟我说了一句话,此时我需要决定回复说什么,需要花费我的精力。
她跟说我了一句话,我也回复她了。她去忙她的事了,我也去忙别的,但是我的脑海里还一直重复决定回复同一句话。那我岂不是要被累死了。
所以这时候我就需要一个像useMemo这样的功能来帮我,当女朋友来找我说话的时候才决定回她什么:
const Me = ({ girlFriendWords }) => {
// 假设girlFriendWords是一个字符串
const myReply = useMemo(
() => decideWhatToSay (girlFriendWords),
[girlFriendWords]
)
return {myReply}
}
复制代码
好了,这样我才能安心的去忙别的了。
在上例中,() => decideWhatToSay (girlFriendWords)函数,根据useMemo的第二个参数数组girlFriendWords,来判断函数是否执行。这样就能保证避免高开销的计算重复计算。
随意使用 useMemo、useCallBack的开销
最后来讨论一个重点问题,在开发过程中可以随意使用useMemo或者useCallBack吗?
引用一段React文档对这个问题的回答:你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。
为什么? 举个例子:
const App = ({ num }) => {
const result = num + 1
return
result
const calculation = () => {
return num + 1
};
const result = useMemo(
calculation,
[num]
)
return {result}
}
复制代码
这个例子只是进行一个简单的计算,可是加了这么多的代码。每一行多余代码的执行都产生消耗,哪怕这消耗只是 CPU 的一丁点热量。
而且使用 useMemo 后,每次执行到这里内部要比对值是否变化,还有存一下之前的函数,消耗更大了,所以使用useMemo来写该场景下的组件,我们并未获得任何收益(函数还是会被创建),useMemo用的越多,负重越多。
站在 Javascript 的角度,当组件刷新时,未被useMemo包裹的方法将被垃圾回收并重新定义,但被useMemo所制造的闭包将保持对回调函数和依赖项的引用。
总结
1.我们应该先写没有优化的代码,优化总是有代价的。遇到需要优化时,再进行优化。
2.在使用时memo,需要注意是否传了引用类型,如果用了要保证传给子组件的props是合理的,不做无用的优化。