前言
在 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是否在另一个对象里,如果在,则判断两个对象相同key
的value
值是否相等
- 判断两个对象的长度是否相等(通过
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
在这个例子中,objB
有 name
和 age
这两个自有属性,因此 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
是从原型链继承来的,而 age
是 employee
自己定义的属性,所以返回 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
,因为age
是employee
自有的属性。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
和 ===
在大多数情况下行为相同,但是它们有两个显著的不同点:
NaN
:在===
中,NaN
不等于NaN
,但是在Object.is
中,NaN
等于NaN
。+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;
的含义是判断 objA
和 objB
是否是相等的,并在它们相等时返回 true
。这里使用的是严格相等运算符 ===
。
-
===
运算符:- 严格相等 :
===
是严格相等运算符,它会同时比较值和类型。如果两个操作数的类型不同,返回false
。 - 对于引用类型(例如对象、数组、函数等),
===
比较的是内存地址,即两个变量是否引用同一个对象。
- 严格相等 :
-
对象的比较:
- 如果
objA
和objB
都是对象类型(包括数组、函数等),===
会检查它们是否指向同一个对象。 - 如果它们指向同一个内存地址(即是同一个对象),则返回
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 的引用,指向同一个对象