memo

在 React 中,父组件的重新渲染会导致子组件的重新渲染。memo 允许我们在 props 不变的情况下避免渲染子组件。

语法

memo(Component, arePropsEqual?):包装一个组件,并获得改组件的缓存版本。

Component: 要包装的组件。 arePropsEqual(prevProps, nextProps): 接收两个参数,前一次的 props 和后一次的 props。返回值是一个布尔类型,true表示新旧 props 相等,false表示两次 props 不相等。

下面用一个例子open in new window感受它的用法。

缓存子组件的例子

import React, { useState, memo } from 'react';

function Hello({ text }) {
  console.log('子组件重新渲染');
  return <h1>{`hello ${text}!`}</h1>;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  const onAddCount = () => {
    setCount((count) => count + 1);
  };

  const onChangeText = () => {
    setText('world');
  };

  return (
    <>
      <span>{count}</span>
      <button onClick={onAddCount}>+1</button>

      <Hello text={text} />
      <button onClick={onChangeText}>改变子组件文本</button>
    </>
  );
}

当我们点击 +1按钮时,会打印 子组件重新渲染。也就是说当我们的父组件更新的时候,子组件也会相应更新。

但是如果我们用 memo 来包裹子组件,代码如下:

import React, { useState, memo } from 'react';

const Hello = memo(function Hello() {
  console.log('子组件重新渲染');
  return <h1>Hello, world!</h1>;
});

// ...

当我们点击 +1按钮时, 子组件重新渲染 不会再打印。也就说我们通过 memo 实现了子组件的缓存。

需要注意的是,当上下文或者子组件内部状态变化的时,依然会触发更新。 memo 缓存组件只是针对 props 不发生改变的情况。

prop 是对象、数组或函数的情况

当传递给子组件的 prop 是对象、数组或函数时,由于它们是引用类型,父组件重新渲染会导致它们被重新定义。也就是说,props 发生了变化。这种情况下,依然会触发子组件更新。

比如下面这个例子open in new window

import React, { useState, memo } from 'react';

const List = memo(function List({ list }) {
  console.log('子组件重新渲染');
  return (
    <>
      {list.map((item) => (
        <div key={item.id}>{item.content}</div>
      ))}
    </>
  );
});

export default function App() {
  const [title, setTitle] = useState('父组件');
  const [todoList, setTodoList] = useState([
    { id: 1, content: '吃饭', isDone: true },
    { id: 2, content: '睡觉', isDone: false },
    { id: 3, content: '洗澡', isDone: true },
    { id: 4, content: '刷牙', isDone: false },
    { id: 5, content: '刷抖音', isDone: false }
  ]);

  const changeTitle = () => {
    setTitle('父组件' + Math.random().toFixed(2));
  };

  const list = todoList.filter((item) => item.isDone);

  return (
    <>
      <h1 onClick={changeTitle}>{title}</h1>
      <List list={list} />
    </>
  );
}

点击父组件,依然会触发子组件渲染。这是由于每次父组件渲染都会重新定义一个变量 list, 两次的 list 不是同一个引用。

这种情况要怎么处理才能避免子组件渲染呢?有两种办法:

1. 使用比较函数

我们可以给 memo 添加第二个参数arePropsEqual:

// ...
(prevProps, nextProps) => {
  return (
    prevProps.list.length === nextProps.list.length &&
    prevProps.list.every((item) => {
      let allOk = true;
      for (let key in item) {
        if (prevProps[key] !== nextProps[key]) {
          allOk = false;
        }
      }
      return allOk;
    })
  );
};
//   ...

这样,当修改 title 时,list 的内容没有变化,并不会触发子组件更新。

个人建议,尽可能避免使用比较函数。主要出于两个考虑:一来别人需要阅读你的比较函数来确定你的组件更新规则;二来我们重写比较函数就意味着每次父组件更新都会执行比较函数。如果比较函数比较复杂且耗时,那么使用比较函数就不再是好的选择了。

2. 使用 useCallback 或者 useMemo 来缓存引用类型

useCallback 用来缓存一个函数。在这个例子里,使用 useMemo 比较合适。

修改 list 的定义,代码如下:

// ...
// 使用useMemo缓存list, 这样title改变不会再触发子组件渲染
const list = useMemo(() => todoList.filter((item) => item.isDone), [todoList]);
// ...

这样,由于我们缓存了 list, 当修改 title 时,list 仍为同一个 list,并不会触发子组件更新。