react memo详解

最近在接触到的几个项目发现,有的项目使用很多地方用到了 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 对性能的提示效果才会更明显。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容