PureComponent和Component的区别---涉及源码

“晚上下班回家,时间充裕的情况下会看react的源码,虽然感觉看的时候就像屎山。今天就来讲解一下PureComponent和Component究竟有什么区别,其实就是带领大家了解一下相关的源码涉及到什么。”

1. 区别

在讲解源码之前,首先我们需要会用,然后在熟悉之后才来说了解源码。 首先讲一下它们之间的区别。当使用Component时,父组件的state或者props更新时,无论子组件的state或者props是否更新,都会导致子组件的更新,因此会造成render渲染,浪费许多性能。这个时候就出现了PureComponent的使用,它其实就是在shouldComponentUpdate进行了浅层比较,减少了不必要的render渲染。
可能你会很好奇为什么是浅层比较而不是深层比较?因为是对性能考虑,在js中对引用类型(浅、深层次引用)的比较其实是需要通过递归比较的。因此如果在组件生命周期中采用递归比较state、props对象,这样会产生较大的性能消耗,所以采用了折中处理,即浅比较。

2. 源码解析

接下来就进入到激动人心的源码解析环节,在这里我不会挨着介绍源码,会挑重点来讲解,如果你对React源码很感兴趣的话那么可以自己研究。
首先找到PureComponent的源码其中有一行很关键的代码pureComponentPrototype.isPureReactComponent = true;在原型上添加isPureReactComponent字段且设置值为true来区分是Component还是PureComponent;

找到ReactFiberClassComponent.new.js文件下的checkShouldComponentUpdate函数下的如下代码:

1
2
3
4
5
6
7
8
// 此时如果你是PureComponent组件时在其原型链上找到isPureReactComponent为true
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
// 这个时候就会进入到shallowEqual浅层比较,这个函数就是PureComponent最精髓的一点
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true; // 默认是返回true

接着我们找到shallowEqual函数,得到代码如下所示,此时我们会在源码中看到is函数,然后你可以跳到下一步中看is函数的解析然后再回过头来看shallowEqual函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 借用原型链上正真的hasOwnProperty方法
const hasOwnProperty = Object.prototype.hasOwnProperty;
function shallowEqual(objA, objB) {
/**
* 我们在看了is函数的封装之后可以知道is函数可以对基本数据进行精确的比较,
* 但是有一点会出现问题:对象引用时,如果只是改变对象中的某个属性值而引用指向不变时,
* is会判断为true,因为它们是指向相同的地址,就像:
* let o1 = {a: 1}; let o2 = o1; o1.a = 'hello';
* o1和o2中a的值都变为hello,它们之间的引用地址都是指向同一个,即o1 === o2为true
*/
if (is(objA, objB)) {
return true;
}
// objA或者objB不为对象时返回false
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// objA和objB都为对象时对key的长度进行比较,如果它们的长度不相等时返回false
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
/**
* key长度相等时,去循环比较objB对象中是否有objA的key值,如果有
* 那么就会去比较对象之间的value值是否相等。
*/
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;
}

is函数代码如下所示,在源码中我给了相关的解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function is(x, y) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
}

// 对于上面的源码我们可以进行一个拆解(其实就是处理基本类型)
function is(x, y) {
if (x === y) {
/**
* 考虑到-0和+0的情况,1/0为Infinity,1/-0为-Infinity
* Infinity === -Infinity为false,即1/0 === 1/-0为false */
return x !== 0 || 1 / x === 1 / y;
} else {
/**
* 还有谁不等于本身呢?答案肯定是NaN,此情况是处理NaN === NaN的情况
* 因此当x为NaN,并且y为NaN时下面会返回true */
return x !== x && y !== y;
}
}

对于上面的源码我就大致介绍到这里,其实还是很简单的。在这里我们需要注意的是:在上面两个对象进行浅层比较时也只是使用Object.is将对象的value进行基本数据的对比;如果对象是深层次的,比如:let obj = {a: {b: ‘hello world’}};那么我们的is函数的判断可能会出现不符合你预想的情况。
深层次比较可能会不准确,比如obj1 = {a: {b: 'hello world'}}obj2 = {a: {b: 'hello world'}},它们虽然外表看起来一样的,但是由于是引用类型它们比较的是地址,此时obj1和obj2引用不是同一个,因此is函数判断obj1和obj2时会返回false,即!is(objA[keysA[i]], objB[keysA[i]])会返回true,最终shallowEqual函数返回false,checkShouldComponentUpdate函数返回true,导致shouldComponentUpdate函数会返回true,导致组件出现无意义的更新。