Warcry mpi
  1. 1 Warcry mpi
  2. 2 Hypocrite Nush
  3. 3 Last Surprise Lyn
  4. 4 BREAK IN TO BREAK OUT Lyn
  5. 5 Flower Of Life 发热巫女
  6. 6 Libertus Chen-U
  7. 7 かかってこいよ NakamuraEmi
  8. 8 Life Will Change Lyn
  9. 9 One Last You Jen Bird
  10. 10 Time Bomb Veela
2018-06-24 18:50:28

函数式编程的好搭档——Immutable

现代前端没人不知道React,但和React同期出现的一个强大工具了解的人却不多。这个工具可以和React完美配合,但不完全属于React生态圈,也可以独立使用,那就是Immutable.js。其强大之处在于补全了JS中难以实现不可变数据结构的问题,而不可变数据结构又是组件化的精髓之一。这篇文章就谈谈Immutable的应用。

mutable的缺陷

我们都知道JS中的对象都是按共享传递的。举个简单的例子如下:

let obj = {
    a: 1;
};
let clone = obj;
clone.a = 2;
console.log(obj.a) // 2 

虽然有时这种灵活性能方便我们共享数据节约内存,但更多的情况下带来的是难以预测的副作用。为了解决这个问题,一般是采用浅拷贝或深拷贝来复制一个相同的新对象,但大多数时候我们并不需要原有对象的所有属性,尤其是原对象的属性很复杂,通过拷贝来处理少量属性实在是得不偿失,造成相当大的CPU和内存的浪费。Immutable的出现很好的解决了这个痛点。

Immutable.js介绍

原理

Immutable对象和原生JS对象的区别核心在于对Immutable对象的任何修改都会返回一个新的Immutable对象,同时旧对象仍然可用并且保持不变。当然这一点实现的效果就可以看做是每次修改都触发了一次深拷贝,但和深拷贝不同的是,新对象只拷贝被修改的节点以及和该节点关联的上级节点,然后共享其他未改变的节点,由此达到减小性能损耗的目的。实现如下来源自网络的图:

数据类型

在JS中对象也可以细分为数组,函数,Set,Map等等类型。类似的,Immutable中提供了如下7种数据类型:

  • List:有序列表,可以看做是JS中的Array
  • Set:唯一值无序集合,可以看做是JS中的Set
  • Map:无序键值对,可以看做JS中的Map
  • Stack:有序栈,可以看做数据结构的栈
  • OrderedMap:有序键值对
  • OrderedSet:唯一值有序集合
  • Record:类似于JS中的Object,只允许字符串类型的键名。

常用操作

  • 和JS的原生数据类型相互转化
// mutable => immutable
let map = Immutable.Map({});
let list = Immutable.List([]);

// immutable => mutable
map.toObject();
list.toArray();
  • 判断两个Immutable对象是否相等
let obj1 = Immutable.Map({a: 1});
let obj2 = Immutable.Map({a: 1});
Immutable.is(obj1, obj2) // true
  • 深度取值和赋值
let map = Immutable.Map({a: {b: {c: 1}}});
Immutable.getIn(map, ['a', 'b', 'c']) // 1
Immutable.setIn(map, ['a', 'b', 'c'], 2) // {a: {b: {c: 2}}}
  • 浅合并和深度合并
// 浅合并
let map1 = Immutable.Map({ a: {b: 1}});
let map2 = Immutable.Map({ a: {c: 2}});
let map3 = Immutable.merge(map1,map2);
// { a: {c: 2}}

//深度合并
let map1 = Immutable.Map({ a: {b: 1}});
let map2 = Immutable.Map({ a: {c: 2}});
let map3 = Immutable.mergeDeep(map1,map2);
// { a: {b: 1, c: 2}}
  • 懒操作

其他数据类型都可以转化为一种特殊结构——Seq,其在原有类型基础上增强了延迟计算的功能。

Immutable.Range(1, Infinity)
.map(n => -n)
// 报错

Immutable.Range(1, Infinity)
.take(2)
.reduce((r, n) => r + n, 0); 
// 3

优势

补全可变数据的不稳定问题

可变数据经过未知函数处理后就不能确定它的状态了,后续的操作就可能出问题。当然大多数情况下我们也是不可能用未知函数处理数据的,但必须考虑到,一个项目是多人维护并迭代的,可能其他成员对于某函数的改动就影响到了另一处使用到该函数的可变数据变动。举例如下:

function test() {
    let data = {key: value}; // 可变数据
    doSomething(data); // 调用外部函数
    ... // 后续逻辑,当data变动后可能受到影响
}

如果不知道doSomething的细节,我们是不能确定data会有哪些改动的,进而影响到后续逻辑。但如果data是Immutable的话就能放心的往下执行。

由此带来最直接的好处就是可以更从容的处理时间和值耦合的场景,比如缓存,数据更新检查,状态回退等。

节约内存

之前也提到了Immutable由于实现了节点共享,所以能够有效的节约内存。

缺陷

Immutable的缺陷可以总结为一点:侵入性过强。因为相当于改变了JS中的整套对象机制,结果就是弥补了JS对象的缺点,但同时也抛弃了JS对象的优势,比如ES6中方便的解构,拓展操作符等等。当引用第三方组件时,如果需要对象参数,又不得不进行转换,相当于又会增加一些业务无关的冗余代码。

在React中的应用

React的渲染核心就是UI = f(state)。可以说组件视图的渲染本质上就是函数式用法,React要求render函数必须是纯函数,不能修改传入props等等都是在强调这个规范,所以Immutable这个来自于函数式编程的概念,自然也能很好的和React配合了。

React和Vue有一个关键不同点在于组件重渲染时,内部是不知道props是否变化的,这意味着每次React组件都会去进行更新流程,即使组件并没有状态变化,因此React内置了shouldComponentUpdate生命周期方法让我们决定更新组件的时机以优化性能,通常我们会去检测state和props是否变化来决定,React也内置了PureComponent帮我们统一改写shouldComponentUpdate中的检测逻辑。源码如下:

if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

可以看到,PureComponent中的比较也只是止步于浅比较,这就意味着嵌套对象,数组等会在比较中被忽略。虽然丢失了一定的精确度,但由于深比较是件很费性能的事,所以算是一种妥协方案。但Immutable由于其结构优势,我们可以相当轻松的避开深比较去检测数据是否变化,既能提升比较精确度,又极大降低了深比较带来的性能消耗,算是相当合适的方案了。写法如下:

import { is } from 'immutable';

shouldComponentUpdate: (nextProps, nextState) => {
  const thisProps = this.props || {}, thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if (!is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (thisState[key] !== nextState[key] && !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}

小结

以上可以看出,Immutable带来的优势和缺陷都是非常明显的,是否使用该方案的关键点就在于对JS对象侵入性改造带来的优势是否足以弥补损失的一些特性。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「React」「JavaScript」「工程化」「Functional Programming」 标签。