PureComponent 使用注意事项以及源码解析

本文简要介绍了 React 中 PureComponent 与 Component 的区别以及使用时需要注意的问题,并在后面附上了源码解析,希望对有疑惑的朋友提供一些帮助。

前言

先介绍一下 PureComponent,平时我们创建 React 组件一般是继承于 Component,而 PureComponent 相当于是一个更纯净的 Component,对更新前后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件重新进行 render。因此可以大大提高组件的性能。


对比 Component 和 PureComponent

继承 Component 创建组件

App.js

里面的 state 有两个属性,text 属性是基本数据类型,todo 属性是引用类型。针对这两种数据类型分别进行对比:

import React, { Component, PureComponent } from 'react';
import './App.css';

class App extends Component {

    constructor(props) {
        super(props)

        this.state = {
            text: 'Hello',
            todo: {
                id: 1,
                message: '学习 React'
            }
        }
    }

    /**
     * 修改 state 中 text 属性的函数
     */
    changeText = () => {
        this.setState({
            text: 'World'
        });
    }

    /**
     * 修改 state 中 todo 对象的函数
     */
    changeTodo = () => {
        this.setState({
            id: 1,
            message: '学习 Vue'
        });
    }

    render() {
        // 打印 log,查看渲染情况
        console.log('tag', 'render');
        
        const { text, todo } = this.state;
        return (
            <div className="App">
                <div>
                    <span>文字:{ text }</span>
                    <button onClick={ this.changeText }>更改文字</button>
                </div>
                <br />
                <div>
                    <span>计划:{ todo.message }</span>
                    <button onClick={ this.changeTodo }>更改计划</button>
                </div>
            </div>
        );
    }
}

export default App;
浏览器中界面

界面显示

测试

运行项目,打开控制台,此时看到只有一个 log:tag render

  • 点击 5 次 ·更改文字· 按钮,可以看到控制台再次多打印了 5 次 log,浏览器中的 Hello 文字变成了 World

  • 点击 5 次 ·更改计划· 按钮,控制台一样多打印 5 次 log,浏览器中的 学习 React 计划变成了 学习 Vue

分析一下,其实 5 次点击中只有一次是有效的,后来的数据其实并没有真正改变,但是由于依然使用了 setState(),所以还是会重新 render。所以这种模式是比较消耗性能的。

继承 PureComponent

其实 PureComponent 用法也是和 Component 一样,只不过是将继承 Component 换成了 PureComponent。

App.js

...
// 上面的代码和之前一致

class App extends PureComponent {
    // 下面的代码也和之前一样
    ...
}

export default App;
浏览器中界面

界面显示

测试

和上面 Component 的测试方式一样

  • 点击 5 次 ·更改文字· 按钮,可以看到控制台只多打印了** 1 次** log,浏览器中的 Hello 文字变成了 World

  • 点击 5 次 ·更改计划· 按钮,控制台只多打印了 1 次 log,浏览器中的 学习 React 计划变成了 学习 Vue

由此可以看出,使用 PureComponent 还是比较节省性能的,即便是使用了 setState(),也会在数据真正改变时才会重新渲染组件

使用时可能遇到的问题

下面我们将代码中 changeTextchangeTodo 方法修改一下

/**
 * 修改 state 中 text 属性的函数
 */
changeText = () => {
    let { text } = this.state;
    text = 'World';
    this.setState({
        text
    });
}

/**
 * 修改 state 中 todo 对象的函数
 */
changeTodo = () => {
    let { todo } = this.state;
    todo.message = "学习 Vue";
    this.setState({
        todo
    });
}

此时我们再重新测试一下:

  • 点击 ·更改文字· 按钮,控制台多打印一次 log,浏览器中的 Hello 文字变成了 World

  • **注意:**点击 ·更改计划· 按钮,控制台没有 log 打印,浏览器中的计划也没有发生改变

为什么代码修改之后,明明 todo 里的 message 属性也已经发生变化了,调用 setState(),却不进行渲染了呢?这是因为 PureComponent 在调用 shouldComponent 生命周期的时候,对数据进行了一次浅比较,判断数据是否发生改变,没发生改变,返回 false,改变了,就返回 true。那这个浅比较的机制是怎么做的呢?我们一起看下面源码解析,来分析一下。


PureComponent 源码解析

ReactBaseClasses.js (Github 代码位置

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
function PureComponent(props, context, updater) {
    this.props = props;
    this.context = context;
    // If a component has string refs, we will assign a different object later.
    this.refs = emptyObject;
    this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

可以看到 PureComponent 的使用和 Component 一致,只时最后为其添加了一个 isPureReactComponent 属性。ComponentDummy 就是通过原型模拟继承的方式将 Component 原型中的方法和属性传递给了 PureComponent。同时为了避免原型链拉长导致属性查找的性能消耗,通过 Object.assign 把属性从 Component 拷贝了过来。

但是这里只是 PureComponent 的声明创建,没有显示如何进行比较更新的,那我们继续看下面的代码。

ReactFiberClassComponent.js (Github 代码位置

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
    ...

    // 这里根据上面 PureComponent 设置的属性 isPureReactComponent 判断一下,如果是 PureComponent,就会走里面的代码,将比较的值返回出去
    if (ctor.prototype && ctor.prototype.isPureReactComponent) {
        return (
            !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
        );
    }
}

shallowEqual 是在 share 包中一个工具方法,看一下其中的内部实现吧。

shallowEqual.js (Github 代码位置

import is from './objectIs';

const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

export default shallowEqual;

这里面还调用了 is 函数,这个函数也是 share 包中的一个工具方法。

objectIs.js (Github 代码位置

/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
    return (
        (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
    );
}

export default is;

PureComponent 源码分析总结

由上面的源码可以发现,其实 PureComponent 和 Component 中的方法和属性基本一致,只不过 PureComponent 多了一个 isPureReactComponent 为 true 的属性。在 checkShouldComponentUpdate 的时候,会根据这个属性判断是否是 PureComponent,如果是的话,就会根据 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) 这个判断语句的返回值作为更新依据。所以,查看了 shallowEqual 和 objectIs 的文件源码,我们可以得出 PureComponent 的浅比较结论:

  • 先通过 is 函数判断两个参数是否相同,相同则直接返回 ture,也就是不更新组件。

    • 根据 objectIs.js 代码可知,基本属性类型判断值是否相同(包括 NaN),引用数据类型判断是否是一个引用
  • 若 is 函数判断为 false,则判断两个参数是否都为 对象 且 都不为 null,若任意一个 不是对象 或 任意一个为 null,直接返回 false,也就是更新组件

  • 若前两个判断都通过,则可断定两个参数皆为对象,此时判断它们 keys 的长度是否相同,若不同,则直接返回 false,即更新组件

  • 若 keys 长度不同,则对两个对象中的第一层属性进行比较,若都相同,则返回 true,有任一属性不同,则返回 false


总结

阅读源码之后,可以发现之前我们修改了 changeTodo 方法的逻辑之后,为什么数据改变,组件却依然不更新的原因了。是因为修改的是同一个对象,所以 PureComponent 默认引用相同,不进行组件更新,所以才会出现这个陷阱,在使用的过程中希望大家注意一下这个问题。

  • 对比 PureComponent 和 Component,可以发现,PureComponent 性能更高,一般有几次有效修改,就会进行几次有效更新

  • 为了避免出现上面所说的陷阱问题,建议将 React 和 Immutable.js 配合使用,因为 Immutable.js 中的数据类型都是不可变,每个变量都不会相同。但是由于 Immutable 学习成本较高,可以在项目中使用 immutability-helper 插件,也能实现类似的功能。关于 immutability-helper 的使用,可以查看我的另一篇博客:immutability-helper 插件的基本使用

  • 虽然 PureComponent 提高了性能,但是也只是对数据进行了一次浅比较,最能优化性能的方式还是自己在 shouldComponent 生命周期中实现响应逻辑

  • 关于 PureComponent 浅比较的总结可以查看上面的PureComponent 源码分析总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值