最近在接触到的几个项目发现,有的项目使用很多地方用到了 React.memo,有的项目一次都没有使用。那么,项目中到底要不要使用 memo 呢?看完这篇文章,你就会有答案。
1、什么是memo
memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
1.1、用法
const Greeting = React.memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
参数
Component:要进行记忆化的组件。memo 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和 forwardRef 组件。
arePropsEqual(可选):一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop。
1.2、与 PureComponent 有何差别
PureComponent 适用于类组件,当 props 和 state 与之前保持一致时会跳过重新渲染。
memo 适用于函数组件,当 props 和之前一致时,会跳过重新渲染。这里不包含 state,React 官方的说法是在函数组件中,即使没有 memo,调用具有相同 state 的 set 函数,默认已经阻止了重新渲染。这句话不太容易理解,翻译一下就是当 setState 的值和引用没有变化时,React 会阻止组件的重新渲染。
2、代码解析
2.1、源码
function memo(type, compare) {
// 核心代码
return {
$$typeof: REACT_MEMO_TYPE,
type: type,
compare: compare === undefined ? null : compare
};
}
memo 的源码比较简单,在返回一个记忆化组件的同时,给组件添加 $$typeof 标记。在创建 Fiber 时,会根据 $$typeof 的值,给 fiber.tag 赋值为 MemoComponent(MemoComponent=14)。
2.2、工作流
当 state(可以是任何组件的 state)改变时,React 会从根节点开始遍历 Fiber 树,在这个过程中会对每一个 Fiber 节点调用 beginWork 方法。当 tag 为 MemoComponent 时,会调用 updateMemoComponent 方法。当我们在使用 memo 没有传入第二个参数时,会调用默认的 shallowEqual 对 prevProps 和 nextProps 进行浅比较,当结果为 true 时,组件就不会重新渲染。
function beginWork(...) {
// ...
switch (workInProgress.tag) {
case MemoComponent: {
return updateMemoComponent(...);
}
}
}
function updateMemoComponent(...) {
// ...
if (updateExpirationTime < renderExpirationTime) {
// Default to shallow comparison
var compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current$$1.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(...);
}
}
var newChild = createWorkInProgress(...);
return newChild;
}
3、注意事项
3.1、当组件属性包含方法和对象时,要结合useMemo和useCallback一起使用。
因为函数组件的特性,当组件state或props发生变化时,其内部的变量会重新声明,如果是引用类型的变量,其引用地址也会发生改变。所以,像下面的用法,达不到预期的效果。
错误用法:
import { memo, useState } from 'react';
export default function MyApp() {
const [name, setName] = useState('');
const [address, setAddress] = useState('');
const userInfo = { name,age:18 };
const onClick = () => {};
return (
<>
<label>
Name{': '}
<input value={name} onChange={e => setName(e.target.value)}/>
</label>
<label>
Address{': '}
<input value={address} onChange={e => setAddress(e.target.value)}/>
</label>
<Greeting userInfo={userInfo} onClick={onClick} />
</>
);
}
const Greeting = memo(function Greeting({ userInfo, onClick }) {
return <h3>Hello{userInfo.name && ', '}{userInfo.name}!</h3>;
});
正确用法:
const userInfo = useMemo(() => {
return { name, age: 18 };
}, [name]);
const onClick = useCallback(() => {
// do something
},[]);
// 使用结合useMemo、useCallback使用
<Greeting userInfo={userInfo} onClick={onClick} />
// 展开对象 仅适用于对象属性都是基本类型的情况
<Greeting {...obj} />
3.2、路由入口组件不要使用memo
因为memo返回的记忆化组件,通常是作为子组件来使用。当页面加载完成,路由入口组件的props,通常是不会再次变化的,所以使用memo包裹,起不到性能优化的效果。
// 路由配置
const routes = [
{
path: '/orderList',
component: '@/pages/orderList',
title: '订单列表'
}
]
// 错误示例
const OrderList = () => {
return (
<div>....</div>
)
}
export default memo(OrderList)
4、结论
说了这么多,那项目中到底要不要使用 memo 呢?React 官方的回答是,当你的代码没有 memo 就无法运行时,再去使用。为什么官方不建议在任何地方都使用 memo 呢,我总结了以下三点原因(个人观点):
1、大部分场景下,memo 对性能提升的效果不是很明显。
2、使代码变得冗余、不易读。
3、占用更多的内存(memo 不会修改原组件,而是返回一个新的、记忆化的组件,需要结合 useMemo、useCallback 等 Hook 一起使用)。
当我们遇到性能问题时,首先要做的是检查代码写得是否有问题,能不能使用其他手段来优化代码,例如,减少不必要的状态提升,而不是直接无脑使用 memo。通常情况下,当你的项目有很多细粒度(绘图、数据可视化、拖拽等)的交互时,使用 memo 对性能的提示效果才会更明显。