【React】PureComponent 和 Component 的区别

前言

在 React 中,PureComponent 和 Component 都是用于创建组件的基类,但它们有一个主要的区别:PureComponent 会给类组件默认加一个shouldComponentUpdate周期函数。在此周期函数中,它对props 和 state (新老的属性/状态)会做一个浅比较只比较第一层状态主要指的是内存地址是否发生变化。如果经过浅比较,发现属性和状态并没有变化,则返回fasle,也就是不继续更新组件,有变化才会去更新。

一、shouldComponentUpdate 使用

javascript 复制代码
 shouldComponentUpdate(nextProps, nextState) {
        let { props, state } = this;
        // props/state:修改之前的属性状态
        // nextProps/nextState:将要修改的属性状态
        return !shallowEqual(props, nextProps) || !shallowEqual(state, nextState);
        // return true 说明需要更新,false 说明不需要更新
    } 

二、浅比较

上图是一个浅比较的逻辑。

这里我们实现一个浅比较的逻辑,主要是比较props

state。首先需要一个判断是否为对象的方法。

  • 判断传过来的参数不为null,且参数类型为object或者function
javascript 复制代码
const isObject = function isObject(obj){
     return obj !== null && /^(object|function)$/.test(typeof obj);
}

比较两个对象的属性和状态

  • 判断传过的对象是否为对象
  • 判断对象是否相等
  • 接下来就开始判断两个对象的key、value
    • 判断两个对象的长度是否相等(通过Reflect.ownkeys(obj)获取key)
    • 循环其中一个对象的key集合,判断该对象的key是否在另一个对象里,如果在,则判断两个对象相同keyvalue 值是否相等
javascript 复制代码
// 对象浅比较的方法
const shallowEqual = function shallowEqual(objA, objB) {
    if (!isObject(objA) || !isObject(objB)) return false;
    if (objA === objB) return true;
    // 先比较成员的数量
    let keysA = Reflect.ownKeys(objA),
        keysB = Reflect.ownKeys(objB);
    if (keysA.length !== keysB.length) return false;
    // 数量一致,再逐一比较内部的成员「只比较第一级:浅比较」
    for (let i = 0; i < keysA.length; i++) {
        let key = keysA[i];
        // 如果一个对象中有这个成员,一个对象中没有;或者,都有这个成员,但是成员值不一样;都应该被判定为不相同!!
        if (!objB.hasOwnProperty(key) || !Object.is(objA[key], objB[key])) {
            return false;
        }
    }
    // 以上都处理完,发现没有不相同的成员,则认为两个对象是相等的
    return true;
};

三、浅比较组件内使用

在使用React.PureComponent时候,组件内会默认加一个shouldComponentUpdate周期函数实现浅比较的功能,但是使用React.Component 要想使用shouldComponentUpdate周期函数的功能,就需要手动添加shouldComponentUpdate,且手动判断属性和状态是否改变。

javascript 复制代码
class Demo extends React.Component {
    state = {
        arr: [10, 20, 30] //0x001
    };
    render() {
        let { arr } = this.state; //arr->0x001
        return <div>
            {arr.map((item, index) => {
                return <span key={index} style={{
                    display: 'inline-block',
                    width: 100,
                    height: 100,
                    background: 'pink',
                    marginRight: 10
                }}>
                    {item}
                </span>;
            })}

            <br />

            <button onClick={() => {
                arr.push(40); //给0x001堆中新增一个40
                // this.setState({ arr }); //最新修改的转态地址,还是0x001「状态地址没有改」 
                // console.log(this.state.arr); //[10,20,30,40]
                /* 
                // 无法更新的
                */

                // this.forceUpdate(); //跳过默认加的shouldComponentUpdate,直接更新
                this.setState({
                    arr: [...arr] //我们是让arr状态值改为一个新的数组「堆地址」
                })
            }}>新增SPAN</button>
        </div >;
    }

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

修改arr的值,并使用setState修改arr的状态,发现页面的状态并没有变化,这是因为,状态变了,但是arr还是之前的状态地址。

javascript 复制代码
  <button onClick={() => {
                arr.push(40); //给0x001堆中新增一个40
                this.setState({ arr }); //最新修改的转态地址,还是0x001「状态地址没有改」 
  }}>新增SPAN</button>

此时想要页面状态改变则有两种方式:

(1)方式一:使用this.forceUpdate(),直接跳过shouldComponentUpdate周期,所以页面一定会更改状态并渲染

javascript 复制代码
            <button onClick={() => {
               arr.push(40); //给0x001堆中新增一个40
               this.setState({ arr }); //最新修改的转态地址,还是0x001「状态地址没有改」 
               this.forceUpdate(); //跳过默认加的shouldComponentUpdate,直接更新
            }}>新增SPAN</button>

(2)方式二:修改arr的状态地址

使用this.setState({ arr: [...arr] }),此时arr的引用地址就不是之前的引用地址,是一个全新的数组,则objA === objB就为false

javascript 复制代码
       <button onClick={() => {
              arr.push(40); //给0x001堆中新增一个40
              this.setState({ arr }); //最新修改的转态地址,还是0x001「状态地址没有改」 
              this.setState({
                   arr: [...arr] //我们是让arr状态值改为一个新的数组「堆地址」
                })
        }}>新增SPAN</button>

四、拓展

1、为什么使用Reflect.ownKeys(obj)获取对象的属性,而不是用Object.keys()?

特性 Object.keys() Reflect.ownKeys()
返回的键类型 仅普通属性的键(字符串) 普通属性键 + 符号属性键(字符串和符号)
返回的属性类型 仅可枚举属性 包含所有自有属性(可枚举与不可枚举属性)
是否支持符号属性
适用场景 处理普通属性,只关心普通属性或只关心可枚举属性 全面访问对象所有自有属性(包括符号属性和不可枚举属性),用于更底层的操作或元编程场景

Reflect.ownKeys(obj) 是一种比 Object.keys(obj) 更通用的方式来获取对象的所有键名,包括普通属性和Symbol 属性。

Object.keys(obj) 只能返回对象的普通属性 (即非符号属性)。它会遍历对象的自有可枚举属性,并返回一个数组。不可枚举属性 (比如通过 Object.defineProperty 设置为不可枚举的属性)和符号属性都不会被返回。

示例:

javascript 复制代码
const obj = {
  normalKey: 'value',
  [Symbol('symbolKey')]: 'symbolValue'
};

console.log(Object.keys(obj));  // 输出: ["normalKey"]

在上述代码中,Object.keys(obj) 只返回了 normalKey,没有返回符号属性。

Reflect.ownKeys(obj) 是一个更通用的方式,它会返回对象的所有自有键名 ,无论是普通属性还是符号属性。返回的结果是一个包含字符串和符号的数组。

示例:

javascript 复制代码
const obj = {
  normalKey: 'value',
  [Symbol('symbolKey')]: 'symbolValue'
};

console.log(Reflect.ownKeys(obj));  // 输出: ["normalKey", Symbol(symbolKey)]

在这个示例中,Reflect.ownKeys(obj) 返回了 normalKey 和符号属性 Symbol(symbolKey),这使得它比 Object.keys(obj) 更为强大和灵活。

示例:结合 Reflect.ownKeys() 使用:

javascript 复制代码
const obj = {
  normalKey: 'value'
};

Object.defineProperty(obj, 'hiddenKey', {
  value: 'hiddenValue',
  enumerable: false
});

const sym = Symbol('symbolKey');
obj[sym] = 'symbolValue';

console.log(Object.keys(obj));  // 输出: ["normalKey"]
console.log(Reflect.ownKeys(obj));  // 输出: ["normalKey", "hiddenKey", Symbol(symbolKey)]

在这个示例中,Object.keys(obj) 只返回了 normalKey,而 Reflect.ownKeys(obj) 返回了所有自有键,包括不可枚举的 hiddenKey 和符号属性 Symbol(symbolKey)

2、为什么使用hasOwnProperty()获取对象的属性,而不是用in?

objB.hasOwnProperty(key) 是 JavaScript 中用于判断对象 objB 是否具有某个自有属性 key 的方法。

hasOwnProperty()Object 的一个原型方法,属于所有对象实例。它用于检查给定的属性是否是对象自身的直接属性,而不是从原型链继承来的属性。

语法

javascript 复制代码
obj.hasOwnProperty(key)

示例 1:检查自有属性

javascript 复制代码
const objB = {
  name: 'Alice',
  age: 25
};

console.log(objB.hasOwnProperty('name'));  // 输出: true
console.log(objB.hasOwnProperty('age'));   // 输出: true
console.log(objB.hasOwnProperty('gender')); // 输出: false

在这个例子中,objBnameage 这两个自有属性,因此 hasOwnProperty('name')hasOwnProperty('age') 返回 true。而 gender 并没有在 objB 上定义,所以返回 false

示例 2:检查继承的属性

javascript 复制代码
const person = {
  name: 'Alice'
};

const employee = Object.create(person);
employee.age = 25;

console.log(employee.hasOwnProperty('age'));   // 输出: true
console.log(employee.hasOwnProperty('name'));  // 输出: false

employee 对象通过 Object.create(person) 创建,继承了 person 的属性。因此,employee.hasOwnProperty('name') 返回 false,因为 name 是从原型链继承来的,而 ageemployee 自己定义的属性,所以返回 true

in 操作符的区别

in 操作符会检查对象及其原型链上是否存在指定的属性,而 hasOwnProperty() 只检查对象自身的属性,不会查找原型链上的属性。

示例 3:区别

javascript 复制代码
const person = {
  name: 'Alice'
};

const employee = Object.create(person);
employee.age = 25;

console.log('age' in employee);           // 输出: true
console.log('name' in employee);          // 输出: true
console.log(employee.hasOwnProperty('age')); // 输出: true
console.log(employee.hasOwnProperty('name')); // 输出: false
  • 'age' in employee 会返回 true,因为 employee 对象自身有 age 属性。
  • 'name' in employee 会返回 true,因为 name 是从原型 person 继承的。
  • employee.hasOwnProperty('age') 返回 true,因为 ageemployee 自有的属性。
  • employee.hasOwnProperty('name') 返回 false,因为 name 是从原型链继承来的。

使用 hasOwnProperty 时的注意点

  • 性能hasOwnProperty() 只检查自有属性,性能上优于检查属性是否存在于对象及其原型链中(例如,使用 in 操作符或 for...in 循环)。
  • 避免属性名冲突 :如果对象本身有一个名为 hasOwnProperty 的属性,可能会导致调用方法时发生冲突。因此,使用 hasOwnProperty 时可以通过 Object.prototype.hasOwnProperty.call() 来确保方法的正确调用:

示例 4:避免属性冲突

javascript 复制代码
const objB = {
  hasOwnProperty: 'some value',
  name: 'Alice'
};

console.log(objB.hasOwnProperty('name'));  // 输出: TypeError: objB.hasOwnProperty is not a function

// 使用 Object.prototype.hasOwnProperty 来避免冲突
console.log(Object.prototype.hasOwnProperty.call(objB, 'name'));  // 输出: true

3、为什么使用Object.is(objA,objB)

Object.is(objA[key], objB[key]) 是 JavaScript 中用于比较两个值是否严格相等的方法,类似于 ===(严格相等操作符),但在某些特殊情况下行为有所不同。Object.is 方法比较的是两个值是否相同,返回 true 表示相等,返回 false 表示不相等。

Object.is=== 在大多数情况下行为相同,但是它们有两个显著的不同点:

  1. NaN :在 === 中,NaN 不等于 NaN,但是在 Object.is 中,NaN 等于 NaN
  2. +0-0 :在 === 中,+0-0 被认为是相等的,但是在 Object.is 中,+0-0 被认为是不同的。

示例 1:NaN 比较

javascript 复制代码
console.log(Object.is(NaN, NaN));  // 输出: true
console.log(NaN === NaN);          // 输出: false

在这个例子中,Object.is(NaN, NaN) 返回 true,而 NaN === NaN 返回 false。这是 Object.is=== 的区别之一。

示例 2:+0-0 比较

javascript 复制代码
console.log(Object.is(+0, -0));  // 输出: false
console.log(+0 === -0);          // 输出: true

在这个例子中,Object.is(+0, -0) 返回 false,而 +0 === -0 返回 true。这是由于 Object.is 会区分 +0-0

使用场景

Object.is 适用于需要精确比较两个值是否完全相同,特别是在以下情况下:

  • 比较 NaN 是否相等。
  • 比较 +0-0 是否相等。
  • 比较对象或数组的引用是否相同。

示例 3:在比较对象属性时使用 Object.is

javascript 复制代码
const objA = { key: 42 };
const objB = { key: 42 };

console.log(Object.is(objA.key, objB.key));  // 输出: true,因为值相等

示例 4:比较引用类型(对象、数组)

javascript 复制代码
const arrA = [1, 2, 3];
const arrB = [1, 2, 3];

console.log(Object.is(arrA, arrB));  // 输出: false,因为它们是不同的引用

const arrC = arrA;
console.log(Object.is(arrA, arrC));  // 输出: true,因为它们引用的是同一个数组

四、使用===判断对象是否相等

if (objA === objB) return true; 的含义是判断 objAobjB 是否是相等的,并在它们相等时返回 true。这里使用的是严格相等运算符 ===

  1. === 运算符:

    • 严格相等=== 是严格相等运算符,它会同时比较值和类型。如果两个操作数的类型不同,返回 false
    • 对于引用类型(例如对象、数组、函数等),=== 比较的是内存地址,即两个变量是否引用同一个对象。
  2. 对象的比较:

    • 如果 objAobjB 都是对象类型(包括数组、函数等),=== 会检查它们是否指向同一个对象。
    • 如果它们指向同一个内存地址(即是同一个对象),则返回 true
    • 如果它们是不同的对象,虽然对象的内容可能相同,但它们的内存地址不同,因此返回 false

示例:

javascript 复制代码
let objA = { name: 'Alice' };
let objB = { name: 'Alice' };
let objC = objA;

console.log(objA === objB); // false, 因为它们是不同的对象,虽然内容相同
console.log(objA === objC); // true, 因为 objC 是 objA 的引用,指向同一个对象
相关推荐
恋猫de小郭2 分钟前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木7 分钟前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮11 分钟前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati13 分钟前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉23 分钟前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain
wuhen_n25 分钟前
双端 Diff 算法详解
前端·javascript·vue.js
UrbanJazzerati25 分钟前
Vue 3 纯小白快速入门指南
前端·面试
雮尘25 分钟前
手把手带你玩转Android gRPC:一篇搞定原理、配置与客户端开发
android·前端·grpc
光影少年25 分钟前
说说闭包的理解和应用场景?
前端·javascript·掘金·金石计划
是一碗螺丝粉26 分钟前
LangChain 核心组件深度解析:模型与提示词模板
前端·langchain·aigc