上一节我们已经知道,shouldComponentUpdate 是我们进行性能优化的利器,我们之后的优化方案都会基于它来进行。
# 优化方案一:PureComponent (memo) 进行浅层比较
上一节我埋下了一个伏笔,就是 PureComponent 或者 memo 将会进行新旧数据的浅层比对。你可能会比较好奇,浅层比较是怎么比较的呢?口说无凭,我觉得让大家直观地感受一下比较重要,所以我暂且扒出 PureComponent 浅比较部分的核心源码让大家体会一下,大家不用紧张,其实逻辑非常简单。
function shallowEqual (objA: mixed, objB: mixed): boolean {
// 下面的 is 相当于 === 的功能,只是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊处理
// 第一关:基础数据类型直接比较出结果
if (is (objA, objB)) {
return true;
}
// 第二关:只要有一个不是对象数据类型就返回 false
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;
}
// 第四关:比较两者的属性是否相等,值是否相等
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;
}
从我写的注释可以看出,在这里开启了四道关卡,但终究还是浅层比较。在下面的情况会判断失灵。
// 调用 state.a.push("2")
state: {a: ["1"]} -> state: {a: ["1", "2"]}
其实 a 数组已经改变了,但是浅层比较会表示没有改变,因为数组的引用没有变。看到没有?一旦属性的值为引用类型的时候浅比较就失灵了。
这就是这种方式最大的弊端,由于 JS 引用赋值的原因,这种方式仅仅适用于无状态组件或者状态数据非常简单的组件,对于大量的应用型组件,它是无能为力的。
# 优化方案二:shouldComponentUpdate 中进行深层比对
为了解决方案一带来的问题,我们现在不做浅层比对了,我们把 props 中所有的属性和值进行递归比对。
我们把上面浅层比对的代码进行一些魔改:
function deepEqual (objA: mixed, objB: mixed): boolean {
// 下面的 is 相当于 === 的功能,只是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊处理
// 第一关:保证两者都是基本数据类型。基础数据类型直接比较出结果。
// 对象类型咱就不比了
if (objA == null && objB == null) return true;
if (typeof objA !== 'object' &&
typeof objB !== 'object' &&
is (objA, objB)) {
return true;
}
// 第二关:只要有一个不是对象数据类型就返回 false
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;
}
// 第四关:比较两者的属性是否相等,值是否相等
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call (objB, keysA [i]) ||
!is (objA [keysA [i]], objB [keysA [i]])
) {
return false;
} else {
if (!deepEqual (objA [keysA [i]], objB [keysA [i]])){
return false;
}
}
}
return true;
}
当访问到对象的属性值的时候,将属性值再进行递归比对,这样就达到了深层比对的效果。但是想想一种极端的情况,就是在属性有一万条的时候,只有最后一个属性发生了变化,那我们就不得已将一万条属性都遍历。这是非常浪费性能的。
# 优化方案 3: immutable 数据结构 + SCU (memo) 浅层比对
回到问题的本质,无论是直接用浅层比对,还是进行深层比对,我们最终是想z知道组件的 props (或 state) 数据有无发生改变。
在这样的条件下,immutable 数据应运而生。
# 什么是 immutable 数据?它有什么优势?
immutable 数据一种利用结构共享形成的持久化数据结构,一旦有部分被修改,那么将会返回一个全新的对象,并且原来相同的节点会直接共享。
具体点来说,immutable 对象数据内部采用是多叉树的结构,凡是有节点被改变,那么它和与它相关的所有上级节点都更新。
用一张动图来模拟一下这个过程:

是吧!只更新了父节点,比直接比对所有的属性简直强太多,并且更新后返回了一个全新的引用,即使是浅比对也能感知到数据的改变。
因此,采用 immutable 既能够最大效率地更新数据结构,又能够和现有的 PureComponent (memo) 顺利对接,感知到状态的变化,是提高 React 渲染性能的极佳方案。
不过有一说一,immutable 也有一些被部分开发者吐槽的点,首先是 immutable 对象和 JS 对象要注意转换,不能混用,这个大家注意适当的时候调用 toJS 或者 fromJS 即可,问题并不大。
其次就是对于 immutable API 的学习成本的争议。我觉得这个问题见仁见智吧,我的观点是:如果你目前沉溺在已经运用得非常熟练的技术栈当中,不说深入学习新技术,连新的 API 都懒得学,我觉得对个人成长来说是一个不太好的征兆。
而且,项目中涉及的 api 并没有那么复杂,完全没必要从头到尾把 immutable.js 的 Api 全都记住。接下来我们就来悉数一下项目将要用到的 immutable 的功能。
# 项目中涉及的 immutable 方法
# 1.fromJS
它的功能是将 JS 对象转换为 immutable 对象。
import {fromJS} from 'immutable';
const immutableState = fromJS ({
count: 0
});
大家以后会经常在 redux 的 reducer 文件中看到这个 api, 是 immutable 库当中导出的方法。
# 2. toJS
和 fromJS 功能刚好相反,用来将 immutable 对象转换为 JS 对象。但是值得注意的是,这个方法并没有在 immutable 库中直接导出,而是需要让 immutable 对象调用。比如:
const jsObj = immutableState.toJS ();
# 3.get/getIn
用来获取 immutable 对象属性。通过与 JS 对象的对比来体会一下:
//JS 对象
let jsObj = {a: 1};
let res = jsObj.a;
//immutable 对象
let immutableObj = fromJS (jsObj);
let res = immutableObj.get ('a');
//JS 对象
let jsObj = {a: {b: 1}};
let res = jsObj.a.b;
//immutable 对象
let immutableObj = fromJS (jsObj);
let res = immutableObj.getIn (['a', 'b']);// 注意传入的是一个数组
# 4.set
用来对 immutable 对象的属性赋值。
let immutableObj = fromJS ({a: 1});
immutableObj.set ('a', 2);
# 5. merge
新数据与旧数据对比,旧数据中不存在的属性直接添加,旧数据中已存在的属性用新数据中的覆盖。
let immutableObj = fromJS ({a: 1});
immutableObj.merge ({
a: 2,
b: 3
});// 修改了 a 属性,增加了 b 属性
好了,到这里项目中为什么要使用 immutable 数据以及基本的使用就给大家讲清楚了