React 性能优化之 Immutable

Immutable 介绍

JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。如 foo={a: 1}; bar=foo; bar.a=2 你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。

为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝) 或 deepCopy(深拷贝) 来避免被修改,但这样做造成了 CPU 和内存的浪费,Immutable 可以很好地解决这些问题。

Immutable 的核心是 不可变(Persistent Data Structure) + 结构共享(Structural Sharing)。所谓 不可变,指的是 Immutable 创建的对象均为不可变对象,对象的任何修改都会返回一个新的对象,原有对象不会发生任何变化。而 结构共享,可以让两个不同的对象共享一部分内存,通过下面这张动图来看会更加清晰:

结构共享.gif

这个动图的场景为通过蓝色的 对象A 生成一个新的 对象B,其中 B 修改了 A 的两个属性(节点),所以实际上 A 和 B 的差异就是这两个被修改的属性,其他属性并没有发生变化,所以 Immutable 能够让没有被修改的属性进行共享,这样 A 和 B 对外而言是两个不同的对象,但是其中相同的部分却是只有一份,占用一份内存!这个特性能够极大的降低内存的消耗。Immutable 的具体实现可参考 精读 Immutable 结构共享

Immutable 降低了 JS Mutable对象 带来的复杂度和隐患,同时能够节省内存,是大型 JS 项目的一剂良药。

React 为什么需要 Immutable

有人说 Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这些至少说明 Immutable 是很有价值的,那 Immutable 为什么能够给 React 带来性能提升呢?当你需要一个东西的时候肯定是它能够弥补你自身的一些缺陷,所以要想知道 React 为什么需要 Immutable,就要知道 React 这方面的缺陷。

React 组件的渲染

React 组件渲染分为初始化渲染和更新渲染。

  • 初始化渲染:在组件第一次挂载的时候会走一次 render 进行渲染,该过程会递归调用所有子组件的 render,如下图,绿色表示已渲染:
初始化渲染.png
  • 不做优化的更新渲染:当我们要更新某个子组件的时候,如下图的绿色组件,从根组件传递下来应用在绿色组件上的数据发生改变:
不做优化的更新渲染.png

默认情况下,那么 React 会调用所有组件的 render,再对生成的虚拟DOM进行对比,如不变则不进行更新。这样的 render 和 虚拟DOM 的对比明显是在浪费,如下图,黄色表示浪费的 render 和 虚拟DOM 对比:

iShot2021-09-13 17.46.13.png
  • 优化后的更新渲染:我们的理想状态是只调用关键路径上组件的 render,如下图:
iShot2021-09-13 17.46.05.png

那么如何避免发生这个浪费,达到理想的更新渲染效果呢,这就要牵出我们的shouldComponentUpdate

shouldComponentUpdate 优化,简称 SCU

React 在每个组件生命周期更新的时候都会调用一个shouldComponentUpdate(nextProps, nextState) 函数。它的职责就是返回 true 或 false,true 表示需要更新,false 表示不需要,默认返回为 true。所以如果不做任何处理,当根组件渲染时会触发所有子组件的渲染。

为了进一步说明问题,我们再引用一张官网的图来解释。绿色表示返回 true 需要更新,红色表示返回 false 不需要更新;vDOMEq 表示 虚拟DOM 比对,绿色表示一致不需要更新,红色表示发生改变需要更新。

根据渲染流程,首先会判断 shouldComponentUpdate(后面简称 SCU) 是否需要更新。如果需要更新,则调用组件的 render 生成新的 虚拟DOM,然后再与旧的 虚拟DOM 对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果 SCU 不需要更新,则直接保持不变,同时其子元素也保持不变。

  • C1 根节点,绿色SCU (true),表示需要更新,然后 vDOMEq 红色,表示 虚拟DOM不一致,需要更新;
  • C2 节点,红色SCU (false),表示不需要更新,所以 C4、C5 均不再进行检查;
  • C3、C6 节点同 C1,需要更新;
  • C7 节点同 C2;
  • C8 节点,绿色SCU (true),表示需要更新,然后vDOMEq绿色,表示虚拟DOM一致,不更新DOM。
PureComponent 及其不足

每写一个组件都要重写 SCU 难免有些麻烦,所以 React 提供了一个基础的高性能组件 PureComponent,这个组件是继承自 Component 的,但是其重写了 shouldComponentUpdate 函数,做了一层 state、props 的浅比较(shallow-compare),避免了一部分 re-render

// 这个变量用来控制组件是否需要更新
var shouldUpdate = true;
if (this._compositeType === CompositeType.PureClass) {
    // 用 shallowEqual 对比 props 和 state 的改动
    // 如果都没改变就不用更新
    // shallowEqual 进行一层浅比较
    shouldUpdate =
      !shallowEqual(prevProps, nextProps) ||
      !shallowEqual(inst.state, nextState);
  }

但是,当传入 props 或 state 不止一层,或者为 array 和 object 时,浅比较就失效了。

deepCompare 带来的内存和性能损耗

由于 PureComponent 无法对多层或者复杂的 state 深入对比,为了减少组件的 re-render,另一个途径就是手动重写组件的 shouldComponentUpdate 函数,在其中使用 深拷贝(deepCopy) 和 深比较(deepCompare) 来对比数据是否发生变化,从而避免无必要的 re-render,但 deepCopy 和 deepCompare 一般都是非常耗性能的。

目前我们项目中大部分的 deepCopy 是通过 JSON.stringify() 来实现的,这种方式本质上是对 JS 对象的序列化和反序列化,不仅浪费内存,而且还降低了应用性能,另外就是如果你的对象里有函数,函数无法被拷贝下来,同时也无法拷贝对象原型链上的属性和方法,存在诸多缺陷。高级一点的方法是使用 lodashcloneDeepWith,这个方案能够拷贝函数和原型链上的属性,但是浪费内存的问题并没有解决。

这里要注意,有些项目里面会使用 Object 的解构 {...obj}Object.assign 这两种方式来做拷贝,这两种方式也只能做到浅拷贝。

拥抱 Immutable

综上所述,React 要在 SUC 上面做优化,遇到的重点问题就是 deepCopy 和 deepCompare 所带来的的内存和性能损耗,而 Immutable 能够完美的解决这两个问题。

Immutable 引入了一套全新的数据类型,像 Collection、List、Map、Set、Record、Seq。有非常全面的 map、filter、groupBy、reduce、find 函数式操作方法。同时 API 也尽量与 Object 或 Array 类似。

Immutable 的不可变特性会在对象发生变化后会重新返回一个新的对象,这个新对象与原来对象的引用是不同的,配合结构共享能够让其中相同部分的内容共享内存,极大的降低了 deepCopy 所带来的损耗。同时 Immutable 通过 Immutable.is 能够高效的进行 deepCompare 操作,因为 Immutable 维护了对象的 hashCode,只要对象的 hashCode 相等,值就是一样的,这样的算法避免了深度遍历比较,性能非常好。

但是 Immutable 也并非是完美的,Immutable 提供了一套全新的数据类型,需要学习新的 API,同时其数据类型在使用过程中容易和 JS 原生对象混淆,需要在写代码的过程中做思维转换,对于团队而言有一定的上手成本。

轻量级的 Immutable 之 immutability-helper

immutability-helper 可以理解为轻量级的 Immutable,同样可以实现高性能的 deepCopy,让没有变化的部分共享内存。但是与 Immutable 相比,immutability-helper 对工程的侵入性较小,它使用的还是 原生JS 对象,其原理如下:

const data = {
    list: [
        {
            name: "aaa",
            sex: "man"
        },
        {
            name: "bbb",
            sex: "woman"
        }
    ],
    status: {
        code: 1
    }
}

如果我们要修改 data.list[0].name 属性的值,为了让没有变化的部分共享内存,我们可以这样写:

const newData = Object.assign({}, data, {
    list: [
        {
            name: "bbb",
            sex: "man"
        },
        data.list[1]
    ]
})

这个时候 data 和 newData 中 list[1]status 就是共享内存的,可以通过下面的打印进行验证:

console.log("data.list[1]==newData.list[1]", data.list[1] == newData.list[1]) // true
console.log("data.status==newData.status", data.status == newData.status) // true

这种 deepCopy 的方式就是 immutability-helper 的底层实现,这个方式相当高效,因为使用了JS原生API将没有变化的对象进行引用,只对变化的对象重新覆盖。

但是你会发现,这种写法非常冗余和繁琐,如果对象嵌套层级很多,那我们可能需要写很多引用或者Object.assign,而 immutability-helper 定义了一套语法糖和指令,简化了这种写法,如下是 immutability-helper 的写法:

import update from 'immutability-helper';

const newData = update(data, {
    list: { 0: { name: { $set: "bbb" } } }
});

可以看到我们只需要对需要变更的对象做修改,而没有变化的对象不需要我们做任何处理,immutability-helper 会自动帮我们取出引用进行赋值。其中 $set 是一个指令,表示替换目标对象,除此之外还有数组新增元素 $push、数组删除元素 $unshift 等指令,具体使用可参考 immutability-helper 的官方文档。

除此之外,类似的库还有 immutability-helper-x,相较于 immutability-helper 的法糖还更简洁,都是同一个系列,这里就不多介绍了,感兴趣的可以看看。

总结

Immutable 可以给复杂的 React/RN 应用带来极大的性能提升,但是否使用还要看项目情况,由于侵入性较强,旧项目的引入会带来新的风险和学习成本,所以可以考虑 immutability-helper 库来进行旧项目的优化,新项目则可以尝试下 Immutable

大部分情况下注意以下几点,其实也不用考虑 SCU 的深度优化,默认的 PureComponentReact.memo 就足够应付了:

  • 函数组件需要使用 React.memo 进行优化,或者直接拆成类组件;
  • props 中如果有函数,需要使用 箭头函数 或者 bind 直接进行绑定,不要每次都构造一个新函数;
  • props 中如果有 style 样式,尽量使用 StyleSheet 定义的静态属性,如果实在需要根据上下文变化的样式,则需要在组件中重写 SCU 来进行特定优化,避免频繁触发 render
  • 如果 props 不会影响子组件的渲染,那么可以直接让子组件的 shouldComponentUpdate 返回 false;
  • 组件尽量拆分的细一些,避免单个组件中数据层次过深,最好保证单个组件中数据层次只有一层,这样就不用考虑可变对象带来的影响了。

本文为原创,转载请注明出处

参考
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容