React 源码:性能优化—— PureComponent 和 memo

最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了

放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完

最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步

它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码

React 源码系列:

本篇是介绍 react 两个性能优化的组件,PureComponentmemo,它们都是用来优化性能的,那么它们是如何实现的呢?

PureComponent 是用于类组件性能优化的,在 propsstate 没有变化时,就不重新渲染类组件

memo 是函数组件的 PureComponent,用 memo 包裹的函数组件,在 props 没有变化时,函数组件时不会重新渲染的,

由于函数组件没有 state,所以 memo 只能用来优化 props 没有变化的情况

PureComponent

PureComponent 是实现了 shouldComponentUpdateComponent 它是对 stateprops 进行浅比较

什么是浅比较?

就是只比较第一层,如果第一层的值相同,就认为两个对象相同,不会再继续比较下去

那如何实现浅比较呢?

  1. 首先判断两个对象是否相同,如果相同,直接返回 true
  2. 如果两个对象中有一个不是对象,那么直接返回 false
    • 可以用上文中的 getType 函数来判断
  3. 获取两个对象的所有 key,如果 key 的数量不相同,直接返回 false
  4. 遍历其中一个对象,如果这个对象的 key 在另一个对象中不存在,或者这两个对象这个 key 的所对应的 value 不一样,返回 false
  5. 直接返回 true
js 复制代码
function shallowCompare(obj1, obj2) {
  // 如果两个对象相同,直接返回 true
  if (obj1 === obj2) return true;
  // 如果两个对象中有一个不是对象,直接返回 false
  if (getType(obj1) !== "object" || getType(obj2) !== "object") return false;

  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);
  // 如果两个对象的 key 的数量不相同,直接返回 false
  if (keys1.length !== keys2.length) return false;

  for (let key of keys1) {
    // 如果 obj2 中不存在 obj1 的 key,或者 obj1 和 obj2 的 key 对应的值不相同,直接返回 false
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) return false;
  }
  // 直接返回 true
  return true;
}

PureComponent 的实现

js 复制代码
class PureComponent extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return (
      !shallowCompare(this.props, nextProps) ||
      !shallowCompare(this.state, nextState)
    );
  }
}

memo

memo 是用于函数组件,如果 props 没有变化,就不会渲染这个函数组件

使用 memo 包裹后的函数组件返回是个什么呢?

我们将它打印出来

js 复制代码
const MemoChild = memo(function MemoChild(props) {
  return <div>{props.name}</div>;
});
console.log(MemoChild);

打印出来我们看到这是一个 $$typeofSymbol("react.memo") 的对象,这不是虚拟 DOM,它是 memo 对象,它的 type 是一个函数组件

js 复制代码
// 输出
{
  $$typeof: Symbol("react.memo"), // memo 兑现
  compare: null,
  type: () => {}, // 函数组件
}

虚拟 DOM 是一个 JSX 对象,它的 $$typeofSymbol("react.element")

js 复制代码
console.log(<MemoChild />);
// 输出
{
  $$typeof: Symbol("react.element"),  // jsx
  key: null,
  props: {},
  ref: null,
  type: {
    $$typeof: Symbol("react.memo"), // memo 对象
    compare: null,
    type: () => {},   // 函数组件
  }
}

下面就来实现 memo 函数

首先在 react.js 中定义一个 memo 函数,返回一个 memo 对象,这个对象包括三个属性:

  • $$typeof:用来标识这是一个 memo 对象
  • type:函数组件
  • compare:决定是否渲染函数组件,如果返回 false 就渲染,返回 true 就不渲染
    • 正好和 shouldComponentUpdate 函数相反
js 复制代码
function memo(type, compare) {
  return {
    $$typeof: REACT_MEMO,
    type,
    compare,
  };
}

我们在处理 memo 函数时,有两处需要处理:

  • 初次渲染时,需要对 memo 包裹的组件特殊处理
  • 之后在每次更新时,如果是个函数组件,需要进行特殊处理

我们先来看初次渲染时,memo 组件如何处理

初次渲染

初次渲染实在 createDOM 函数中处理

这里还是要强调一遍,<MemoChild /> 这不是 memo 对象,这是 jsx 对象,它的 type 才是 memo 对象

js 复制代码
function createDOM(VNode) {
  let { type, props, $$typeof, ref } = VNode;
  let dom;
  // <MemoChild /> 这不是 memo 对象,这是 jsx 对象,这里会递归处理,第二次进来时,type 是 memo 对象
  if (type && type.$$typeof === REACT_MEMO) {
    return getDomByMemoFunctionComponent(VNode);
  }
  // ...
}

getDomByMemoFunctionComponent 函数比较简单,和处理函数组件 getDomByFunctionComponent 几乎一样,唯一不同的是,typememo 对象,type.type 才是函数组件

js 复制代码
function getDomByMemoFunctionComponent(VNode) {
  let { type, props } = VNode;
  // 和函数不一样的是:type 是 memo 对象,type.type 才是函数组件
  let renderVNode = type.type(props);
  if (!renderVNode) return null;
  VNode.oldRenderVNode = renderVNode;
  return createDOM(renderVNode);
}

更新

相比初次渲染,更新时处理有点复杂

首先在 deepDOMDiff 函数中需要增加对 memo 类型的处理条件

js 复制代码
function deepDOMDiff(oldVNode, newVNode) {
  let diffTypeMap = {
    // ...
    // memo 节点,type 是一个 memo 对象,需要拿到 $$typeof 属性,
    MEMO: oldVNode.type.$$typeof === REACT_MEMO,
  };
  const DIFF_TYPE = Object.keys(diffTypeMap).filter(
    (key) => diffTypeMap[key]
  )[0];
  switch (DIFF_TYPE) {
    // ...
    case "MEMO":
      // 处理 memo 节点
      updateMemoFunctionComponent(oldVNode, newVNode);
      break;
    default:
      break;
  }
}

具体的处理逻辑在 updateMemoFunctionComponent 函数中

具体分为这几步:

  1. 首先我们从 oldVNode 中拿到 type,这个 typememo 对象
  2. 重新渲染有两种情况:
    • 如果 type.compare 不存在,我们则使用 shallowCompare 函数进行浅比较,shallowCompare 返回 false 时,我们才进行重新渲染
      • shallowCompare 函数在 PureComponent 中实现了,它是对 prevPropsnextProps 进行浅比较
    • 如果 type.comparetype.compare 返回 false时,我们才进行重现渲染
  3. 重新渲染时,和函数组件一样,唯一的区别还是:type 不是函数组件,type.type 才是函数组件
js 复制代码
function updateMemoFunctionComponent(oldVNode, newVNode) {
  let { type } = oldVNode;
  // 如果 type.compare 不存在,我们则使用 shallowCompare 函数进行浅比较,shallowCompare 返回 false 时,我们才进行重新渲染
  // 如果 type.compare 且 type.compare 返回 false 时,我们才进行重现渲染
  if (
    (!type.compare && !shallowCompare(oldVNode.props, newVNode.props)) ||
    (type.compare && !type.compare(oldVNode.props, newVNode.props))
  ) {
    const oldDOM = (newVNode.dom = findDOMByVNode(oldVNode));
    if (!oldDOM) return;
    const { type } = newVNode;
    // 和函数不一样的是:type 是 memo 对象,type.type 才是函数组件
    let renderVNode = type.type(newVNode.props);
    updateDomTree(oldVNode.oldRenderVNode, renderVNode, oldDOM);
    newVNode.oldRenderVNode = renderVNode;
  } else {
    // 不重新渲染
    newVNode.oldRenderVNode = oldVNode.oldRenderVNode;
  }
}

总结

  1. PureComponentmemo 都是用来优化性能的,它们都是对 props 进行浅比较,如果 props 没有变化,就不会重新渲染
  2. PureComponent 是类组件使用,可以比较 stateprops,内部实现了 shouldComponentUpdate 生命周期函数,返回 false 不会重新渲染
  3. memo 是函数组件使用,只能比较 props,内部通过浅比较 props 来决定是否重新渲染
    • 它也接收一个 compare 函数,如果 compare 函数返回 true 不会重新渲染

源码

  1. shallowCompare
  2. PureComponent
  3. memo:
相关推荐
檀越剑指大厂13 分钟前
【Python系列】异步 Web 服务器
服务器·前端·python
我是Superman丶15 分钟前
【前端】js vue 屏蔽BackSpace键删除键导致页面后退的方法
开发语言·前端·javascript
Hello Dam16 分钟前
基于 Spring Boot 实现图片的服务器本地存储及前端回显
服务器·前端·spring boot
小仓桑18 分钟前
利用 Vue 组合式 API 与 requestAnimationFrame 优化大量元素渲染
前端·javascript·vue.js
Hacker_xingchen18 分钟前
Web 学习笔记 - 网络安全
前端·笔记·学习
天海奈奈19 分钟前
前端应用界面的展示与优化(记录)
前端
多多*40 分钟前
后端并发编程操作简述 Java高并发程序设计 六类并发容器 七种线程池 四种阻塞队列
java·开发语言·前端·数据结构·算法·状态模式
mubeibeinv1 小时前
列表代码思路
前端
过期的H2O21 小时前
【H2O2|全栈】JS进阶知识(十)ES6(6)
开发语言·前端·javascript·ecmascript·es6
White graces1 小时前
Spring MVC练习(前后端分离开发实例)
java·开发语言·前端·后端·spring·java-ee·mvc