# React 中的组件渲染机制

在了解这个方法之前,需要了解一下 React 中的基本渲染关系。

在 React 当中,组件之间是严格按照父子关系来渲染、传递数据的。如果什么其他的配置都不加,就十分纯粹的写了父组件,内部套了一个子组件。这个父组件有个状态叫 count ,那么调用 setCount 的时候就会触发父组件的更新。 而父组件的重新渲染,会导致子组件无脑进行重新渲染,无论传给子组件的 Props 是否发生了变化。 这有可能会导致组件性能的极大浪费,因为有些父组件的状态可能会不断的发生变化,而子组件有时候需要保留自己的状态(如 input 框)。

# React.memo 是?

为了解决这个子组件无脑随着父组件刷新而刷新的问题,React 就提供了这个方法: React.memo ,当然也可以直接将其解构成 memo 方法来用。

React.memo 是一个高阶组件,其主要作用是对组件进行性能优化。它通过记忆(memoizing)组件的渲染结果,来避免在某些情况下进行不必要的重新渲染。

当使用 React.memo 包裹一个组件时,React 会检查这个组件接收的 props 是否发生变化:如果 props 没有变化,那么 React 将不会重新渲染这个组件,而是复用上一次渲染的结果。这可以显著提高应用的性能,特别是当处理那些渲染开销比较大的组件时。

简而言之, React.memo 是一个优化组件渲染性能的方法,适用于那些纯组件(即组件的输出只依赖于输入的 propsstate ,而不依赖于其他外部状态或副作用)。

但是 React.memo 只比较 props 的浅层变化。如果的组件依赖于深层对象结构的 props,可能需要提供第二个参数,一个比较函数,来 自定义比较逻辑 ,确保组件能够在必要时更新。

# React.memo 的基本使用

React.memo 本身是一个高阶组件,这也就意味着它返回的也是一个 React 组件。它内部可以传递你原本的 React 子组件,如果是函数式组件,直接将这个函数写入 memo 函数体内部即可。

import { memo, useState } from "react";
const MemoSon = memo(function Son() {
  console.log("我是儿子,我渲染了");
  return <div>this is son</div>;
});
const Father = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>change count: {count}</button>
      <MemoSon />
    </div>
  );
};
export default Father;

可以看到这里的 Son 本身就是一个完整的函数式组件,而使用 memo 将其进行包裹后返回了一个新的组件,将其命名为 MemoSon 。其内部的功能还是和原本的组件一样,只是在父组件触发重新渲染的时候,会额外的进行子组件 props 的新旧比较。如果比较没有发生改变,那么不会触发重新渲染;如果检测到新旧的 props 不同,那么就重新进行渲染。

当然,除了粗暴的对 props 整体进行新旧比较之外,当 props 内容比较复杂(比如是一个嵌套很多层的对象)的时候,有可能需要指定这个内容的某个部分法生变化之后才触发组件的重新渲染,这个时候就需要用到它的第二个参数:一个比较函数,来自定义比较规则。

const MemoSon = memo(
  function Son({ user }) {
    console.log("我是儿子,我渲染了");
    return (
      <div>
        <span>this is son</span>
        <p>name: {user.name}</p>
        <p>age: {user.age}</p>
        <p>gender: {user.gender}</p>
      </div>
    );
  },
  (prevProps, nextProps) => {
    if (prevProps.user.name === nextProps.user.name) {
      return true;
    }
    return false;
  }
);
const Father = () => {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({
    name: "jack",
    age: 20,
    gender: "male",
  });
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>change count: {count}</button>
      <MemoSon user={user} />
    </div>
  );
};

在这里又加了一个 user 的 props,这个 props 内部定义了三个字段,我想要自定义规则,只在 name 发生变化之后才触发重新渲染。

这个比较函数语法比较简单: (prevProps, nextProps) => boolean

  • prevProps : 上一次的 props

  • nextProps : 这一次的 props

  • 如果返回 true,则不重新渲染

  • 如果返回 false,则重新渲染

# React 中 props 的新旧比较机制

React 中,当组件的 props 发生变化时,React 会重新渲染组件以反映最新的 props。React 对 props 的比较机制依赖于 JavaScript 的比较逻辑,也就是分 基础类型引用类型

# 基础类型(Primitive Types)

基础类型包括 stringnumberbooleannullundefinedsymbol 等。当这些类型作为 props 传递时,React 通过简单的值比较( === )来检查它们是否发生了变化。由于基础类型是不可变的,这种比较是准确的,如果值没有变化,React 就认为该 prop 没有变化,因此不会触发重新渲染。

// 示例
// 如果 Component 在两次渲染之间,其 prop value 从一个基础类型的值变为另一个相同的值,
// React 通过 `===` 比较认定值没有变化,不会重新渲染 Component。
<Component value="hello" /> // 第一次渲染
<Component value="hello" /> // 第二次渲染,value 未变,不重新渲染

# 引用类型(Reference Types)

引用类型包括 objectarrayfunction 等。当这些类型作为 props 传递时,React 同样使用 === 进行比较,但这里比较的是引用地址而不是值。即使对象或数组的内容没有变,只要引用地址变了,React 就会认为 prop 发生了变化,从而触发组件的重新渲染。

这意味着,如果你在父组件中创建一个对象或数组并作为 prop 传递给子组件,即使数据没有实质的变化,每次父组件渲染时都会创建一个新的引用,导致 React 认为 props 发生了变化,从而重新渲染子组件。

// 示例
// 即使对象内容没有变,但每次渲染都创建了一个新的对象引用,
// 导致 React 认为 props 发生了变化,会重新渲染 Component。
<Component obj=<!--swig0--> /> // 第一次渲染
<Component obj=<!--swig1--> /> // 第二次渲染,obj的内容相同,但引用地址变了,触发重新渲染

# 结合 useMemo 来避免引用类型变化引起的重新渲染

因为 JS 对引用类型的比较是根据引用的不同来判定是否相同的,因此即使你不对引用类型做任何的更改,它在父组件中被定义了之后,父组件重新渲染,会使新的引用类型传给子组件,子组件接到之后因为引用不同也会直接判定是不同的 props,从而触发重新渲染。

先看一下基本的代码:

import { memo, useState } from "react";
const MemoSon = memo(function Son({ list }) {
  console.log("我是儿子,我渲染了");
  return (
    <div>
      <span>this is son</span>
      <ul>
        {list.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});
const Father = () => {
  const [count, setCount] = useState(0);
  const list = [1, 2, 3, 4, 5];
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>change count: {count}</button>
      <MemoSon list={list} />
    </div>
  );
};
export default Father;

可以观察一下渲染的结果:

memo演示1

这时候,使用 useMemo 对引用类型进行缓存,就可以很好的解决这一问题。

import { memo, useMemo, useState } from "react";
const MemoSon = memo(function Son({ list }) {
  console.log("我是儿子,我渲染了");
  return (
    <div>
      <span>this is son</span>
      <ul>
        {list.map((item, index) => {
          return <li key={index}>{item}</li>;
        })}
      </ul>
    </div>
  );
});
const Father = () => {
  const [count, setCount] = useState(0);
  // 使用 useMemo 来缓存引用类型的数据
  const list = useMemo(() => {
    return [1, 2, 3, 4, 5];
  }, []);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>change count: {count}</button>
      <MemoSon list={list} />
    </div>
  );
};
export default Father;

可以观察一下变更后的结果:

memo演示2

# 结语

总而言之,这个 React.memo 方法是一个比较传统的性能优化的函数,在 Vue 中也有类似的函数存在,比如 v-memo 指令、 Composition API 等等也可以实现类似的功能。主要使用在想要保存子组件的状态或者一些嵌套比较深的组件树,并且组件树的顶部比较容易发生变化的场景上。