概述
React.memo和useMemo都是React进行性能优化的手段,它们允许我们缓存需要进行高性能计算的结果,以便下次渲染页面时,返回缓存的值而不必重新计算函数,从而确保我们的应用程序运行的更快,避免不必要的开销。
React.memo 详解
为什么memo(memoization的简写)在React中这么重要呢?
在React的组件中,如果子组件没有被React.memo包裹,或者没有使用useMemo来处理props传递参数,那么当父组件的任何值更新时,整个组件都将会进行重新渲染,包括父组件下面的所有子组件。这对于子组件来说,岂不是非常不友好?毕竟不是父组件的每一次更新都需要修改子组件的值,而频繁的更新却会导致不需要更新的子组件被迫更新,这何尝不是一种资源的浪费。
针对上述问题,React提供了React.memo和useMemo。
我们先来通过一个例子来看React.memo的相关概念和使用:
如果子组件没有用React.memo进行包裹的话,父组件的重新渲染就会导致子组件跟着一起重新渲染:
// 父组件
import {useMemo, useState} from "react";
import ReactMemoChild from "./ReactMemoChild";
export const ReactMemoFather = () => {
const [count, setCount] = useState(0);
return (
<>
<p>按钮点击次数:{count}</p>
<ReactMemoChild/>
<button onClick={() => setCount(n => n + 1)}>按钮</button>
</>
);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
// 子组件
import React, {useRef} from "react";
function ReactMemoChild() {
const ref = useRef(0);
console.log('子组件重新渲染');
return (
<>
<p>页面渲染次数:{ref.current++}</p>
</>
);
}
export default ReactMemoChild;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
代码解读:
上述代码的含义是:在父组件ReactMemoFather中添加了一个子组件ReactMemoChild,当点击父组件的button按钮时,会同步触发子组件的重新渲染。
使用useRef是用来测试子组件被重新渲染的次数。
渲染结果如下图所示:
注意,在上述例子中,使用useRef是为了测试渲染次数。在正式开发中,不建议把useRef输出的值直接绑定到需要频繁更新的页面DOM元素上,因为useRef对应的生命周期钩子函数是shouldComponentUpdate,这个是在render之前的,render之前可能会有多次渲染,从而导致useRef执行多次,因此就如上图所展示的,子组件的页面渲染次数是偶数次增加的。
接下来,我们用React.memo把子组件进行包裹:
import React, {useMemo, useRef} from "react";
function ReactMemoChild() {
const ref = useRef(0);
return (
<>
<p>页面渲染次数:{ref.current++}</p>
</>
);
}
export default React.memo(ReactMemoChild);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
此时,无论父组件如何点击,子组件都不会进行重复渲染了。
同时,也可以通过props传递的参数值的变化来触发子组件的更新~
这样就有效的避免了子组件无效的重复更新!
对应到一个稍显复杂的业务场景,比如一个父组件下面有数以千计的子组件,如果没有借助React.memo或者useMemo的话,每次父组件的更新都会触发子组件的更新,这岂不是对性能消耗非常大!
通过上面的实例,我们可以总结一下React.memo:
React.memo()本质是一个高阶组件(HOC),高阶组件和高阶函数类似,高阶函数是接收一个函数,然后经过一些判断和处理后再返回这个函数,比如我们常见的防抖、节流函数。
对应到高阶组件,就是接收一个组件,然后经过一些判断和处理后再返回这个组件。
再回归到React.memo(), 这个高阶组件接收一个组件A作为参数并返回一个组件B,如果组件B的props没有改变,则组件B会阻止组件A重新渲染。A和B本质上是同一个组件,但A是否进行重新渲染,需要由Props是否发生改变来决定。
接下来说说useMemo
同样,我们就使用React.memo的那个例子来看看如果是用useMemo的话是如何实现的:
// 父组件
import {useState, useRef, useMemo} from "react";
import UseMemoChild from "./UseMemoChild";
export default function UseMemoFather() {
const [count, setCount] = useState(0);
const [times, setTimes] = useState(0);
const useMemoRef = useRef(0);
const incrementUseMemoRef = () => useMemoRef.current++;
const memoizedValue = useMemo(() => incrementUseMemoRef(), [times]);
return (
<div>
<div>
<p>按钮点击次数:{count}</p>
<button onClick={() => setCount(count + 1)}>按钮</button>
<button onClick={() => setTimes(times + 1)}>
Force render
</button>
<UseMemoChild memoizedValue={memoizedValue}/>
</div>
</div>
);
}
- 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
// 子组件
interface PropType{
memoizedValue: number
}
function UseMemoChild({memoizedValue}: PropType) {
return (
<div className="mt-3">
<p className="dark:text-white max-w-md">
I'll only re-render when you click <span className="font-bold text-indigo-400">Force render.</span>
</p>
<p className="dark:text-white">I've now rendered: <span className="text-green-400">{memoizedValue} time(s)</span> </p>
</div>
);
}
export default UseMemoChild;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
这一次,我们没有用React.memo来包裹子组件,而是采用了把需要传递给子组件的参数用useMemo进行处理,从而实现了子组件的更新只发生在传递给子组件的参数发生变化的时候。
那useMemo是干什么用的呢?
之前的文章其实深入讲解过useMemo,从本文提到的这个例子来看,相比于React.memo是一个高阶组件来说,useMemo其实充当的是React提供的一个hook,使用useMemo定义的变量,只会在useMemo的第二个依赖参数发生修改时才会发生修改。
useMemo对应的生命周期钩子函数是shouldComponentUpdate,当useMemo依赖的参数没有发生改变时,shouldComponentUpdate为false,从而就阻止了子组件的渲染~
按照React官网的建议,我们使用useMemo时,应保证第一个参数函数里所使用的变量都出现在第二个依赖参数数组中,这样可以避免一些额外的错误。本文提到的例子是为了测试子组件的渲染,在真实的开发中,其实是不建议useRef和useMemo的结果直接绑定到需要频繁更新的Dom上的。
React.memo&useMemo的异同点
相同点:
- 它们都可以用来缓存数据,避免子组件的无效重复渲染。
不同点:
- React.memo是一个高阶组件,useMemo是一个hook。
联系:
- 当我们的父子组件之间不需要传值通信时,可以选择用React.memo来避免子组件的无效重复渲染。
- 但我们的父子组件之间需要进行传值通信时,React.memo和useMemo都可以使用。
React.memo、useMemo、useCallback、useRef都是React进行性能优化的手段,不过我们一定要记得合理运用,不能过度使用,因为深究这几个方法的实现其实都是借助了闭包,会一直占用我们的内存,运用不当可能会导致反向的性能优化问题~
扩展:
细心的同学在测试本文的代码时,应该会发现,每次useState使得父子组件重新渲染时,在子组件中用来测试渲染次数的useRef会执行两次。刚开始我不太理解,不过百度一番后,在React的官方文档里找到了答案:React严格模式.
按照官方文档的解释,严格模式的React在执行阶段会检测意外的副作用,这意味着React可以在提交之前多次调用渲染阶段生命周期的方法,从而导致useRef执行多次,不过这个问题只出现在开发环境下,正式的生产环境下不会有这个问题。