细读 React | PureComponet

发布于 2023-05-13 13:30:14 字数 10079 浏览 93 评论 0

配图源自 Freepik

今天来聊一聊 React.ComponentReact.PureComponentReact.memo 的一些区别以及使用场景。

一、类组件定义

在 React 中,可以通过继承 React.ComponentReact.PureComponent 来定义 Class 组件:

import React, { Component, PureComponent } from 'react'

class Comp extends Component {
  // ...
}

class PureComp extends PureComponent {
  // ...
}

两者很相似,区别在于 React.Component 并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 propstate 的方式来实现了该函数。

如果赋予 React 组件相同的 propsstaterender() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

注意:React.PureComponent 中的 shouldComponentUpdate() 仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的 propsstate 较为简单时,才使用 React.PureComponent,或者在深层数据结构发生变化时调用 forceUpdate() 来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。

此外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

二、浅层对比实现

我们来看下源码,它们是如何“浅层对比”的?

首先,在非强制更新组件的情况下,若 propsstate 的变更,内部都会触发 checkShouldComponentUpdate 方法来判断是否重新渲染组件。若使用 forceUpdate() 强制更新组件的话,则会跳过该方法。

// checkHasForceUpdateAfterProcessing 方法用于判断是否强制更新
// 若不是强制更新,则会根据 checkShouldComponentUpdate 方法判断是否应该更新组件
var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);
function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
  var instance = workInProgress.stateNode;

  // 若自实现了 shouldComponentUpdate 方法,则不会跑到后面的步骤
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');
    var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);
    stopPhaseTimer();

    {
      !(shouldUpdate !== undefined) ? warningWithoutStack$1(false, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', getComponentName(ctor) || 'Component') : void 0;
    }

    return shouldUpdate;
  }

  // 关键是这里:
  // 在 React 组件未实现 shouldComponentUpdate 前提下,
  // 可通过 isPureReactComponent 判断是否为 PureComponent 组件的原因是构造函数里设置了该属性的值为 true。
  // 使用 shallowEqual 方法来判断组件属性和状态时是否发生了变化,若两种均是“相等”,则返回 false,即不更新组件,否则会触发组件的 render() 方法以更新组件。
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
  }

  return true;
}

再看下 shallowEqual 的实现,不难:

function shallowEqual(objA, objB) {
  // is$1 相当于 ES6 的 Object.is() 方法,比较两个操作数是否相等
  if (is$1(objA, objB)) {
    return true;
  }

  // 讲过上一步的排除之后,若 objA 或 ObjB 的值是“非引用类型”或 null,则可以确定 objA 与 objB 是不相等的。
  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }

  // 走到这步,说明 objA 和 objB 是两个不同的引用类型的值
  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);
  
  // 比较两者的属性数量是否一致,若不一致,则可确定两者是不相等的
  if (keysA.length !== keysB.length) {
    return false;
  } // Test for A's keys different from B.

  // 这里只遍历最外层的属性是否一致
  for (var i = 0; i < keysA.length; i++) {
    // hasOwnProperty$2 即 Object.prototype.hasOwnProperty;
    // 先比较 objA 的属性,在 objB 属性有没有,若无说明两者不相等,否则接着再判断同一属性值是否相等,
    // 这判断就比较简单了:Object.is() 是使用全等判断的,并认为 NaN === NaN 和 +0 !== -0 的。
    if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  }

  // 否则,返回 true,认为它们相等。
  return true;
}

默认浅层对比方法,相当于:

shouldComponentUpdate(nextProps, nextState) {
  return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}

关于 Object.is() 解决了什么“奇葩”问题,可以看此前的一篇文章:JavaScript 相等比较详解 的第三节内容。

根据以上源码的分析,可以得出结论:

  • 若基于 React.PureComponent 的组件自实现了 shouldComponentUpdate() 方法,则会跳过默认的“浅层对比”,可以理解为覆盖了默认的 shouldComponentUpdate() 方法。。

  • 从源码可知,React.Component “未实现” shouldComponentUpdate() 是因为内部返回了 true 而已。

  • React.PureComponent 的浅层对比,主要分为三步判断:1️⃣ 对比 oldPropsnewProps 是否相等,若相等则返回 false,否则继续往下走;2️⃣ 接着判断 oldPropsnewProps (此时可以确定两者是不相等的引用值了)的第一层属性,若属性数量或者属性 key 不一致,则认为两者不相等并返回 true,否则继续往下走;3️⃣ 判断对应属性的属性值是否相等,若存在不相等则返回 true,否则返回 false。对于 oldStatenewState 的判断同理。

    注意:这里提到的返回值 true/false 是指 !shalldowEqual() 的结果,相当于 shouldComponentUpdate() 的返回值

三、示例及注意事项

基于以上结论,来看几个示例吧。

先明确几点:

  • 使用 setState() 来更新状态,无论状态值是否真的发生了改变,都会产生一个全新的对象,即 oldState !== newState
  • 组件的 props 对象是 readonly(只读)的,React 会保护它不被更改,否则会出错。
  • 每次父组件的重新渲染,子组件的 props 都会是一个全新的对象,即 oldProps !== newProps
  • 一般情况下,组件实例的 props 值几乎都是一个引用类型的值,即对象,我还没想到有什么场景会出现 null 的情况。而组件实例的 state 值则可能是对象或 null,后者即无状态的类组件,当然这种情况下应可能使用函数组件。
// 父组件
class Parent extends React.Component {
  state = {
    number: 0, // 原始类型
    list: [] // 引用类型
  }

  changeList() {
    const { list } = this.state
    list.push(0)
    this.setState({ list })
  }

  changeNumber() {
    this.setState({ number: this.state.number + 1 })
  }

  render() {
    console.log('---> Parent Render.')
    return (
      <>
        <button onClick={this.changeNumber.bind(this)}>Change Number</button>
        <button onClick={this.changeList.bind(this)}>Change List</button>
        <Child num={this.state.number} lists={this.state.list} />
      </>
    )
  }
}


// 子组件
class Child extends React.PureComponent {
  state = {
    name: 'child'
  }

  render() {
    console.log('---> Child Render.')
    return (
      <>
        <div>Child Component.</div>
      </>
    )
  }
}

1️⃣ 当我们点击父组件的 Change Number 按钮时,子组件会重新渲染。因为在对比子组件的 oldProps.numnewProps.num 时,两者的值不相等,因此会更新组件。在控制台可以看到:

---> Parent Render.
---> Child Render.

2️⃣ 当我们点击父组件的 Change List 按钮时,子组件不会重新渲染。因为在对比子组件的 oldProps.listnewProps.list 时,它们都是引用类型,且两者在内存中的地址是一致的,而且不会更深层次地去比较了,因此 React 认为它俩是相等的,因此不会更新组件。在控制台只看到:

---> Parent Render.

当然,这一点也是 React.PureComponent 的局限性,因此它应该应用于一些数据结构较为简单的展示类组件。

另外,React.PureComponent 中的 shouldComponentUpdate() 将跳过所有子组件树的 prop 更新。因此,请确保所有子组件也都是“纯”的组件。

四、延伸 React.memo

如果在函数组件中,想要拥有类似 React.PureComponent 的性能优化,可以使用 React.memo

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
})

React.memo 为高阶组件

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReducer 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}

function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}

export default React.memo(MyComponent, areEqual)

此方法仅作为 性能优化 的方式而存在。但请不要依赖它来 阻止 渲染,因为这会产生 bug。

注意,与 class 组件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 会返回 true;如果 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

简单来说,若需要更新组件,那么 areEqual 方法请返回 false,否则返回 true

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

奢欲

暂无简介

文章
评论
28 人气
更多

推荐作者

佚名

文章 0 评论 0

今天

文章 0 评论 0

゛时过境迁

文章 0 评论 0

达拉崩吧

文章 0 评论 0

呆萌少年

文章 0 评论 0

孤者何惧

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文