jtwang7 / React-Note

React 学习笔记
8 stars 2 forks source link

React - Immutable 数据流 #15

Open jtwang7 opened 3 years ago

jtwang7 commented 3 years ago

参考文章: Immutable 详解及 React 中实践 React性能优化的中流砥柱——Immutable数据流

前言

JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。JS 对象的引用赋值特点虽然可以大大节约栈内存空间,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。Immutable 可以很好地解决这些问题。

Immutable

immutable 数据一种利用结构共享形成的持久化数据结构,一旦有部分被修改,那么将会返回一个全新的对象,并且原来相同的节点会直接共享。

Persistent Data Structure(持久化数据结构):使用旧数据创建新数据时,要保证旧数据同时可用且不变,返回全新的数据。 Structural Sharing(结构共享):如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享,避免了 deepCopy 把所有节点都复制一遍带来的性能损耗。

每次修改一个 immutable 对象时都会创建一个新的不可变的对象,在新对象上操作并不会影响到原对象的数据。 immutable 对象数据内部采用是多叉树的结构,如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。如图:

优点

  1. Immutable 更容易被追踪和回溯 可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。
    function touchAndLog(touchFn) {
    let data = { key: 'value' };
    touchFn(data);
    console.log(data.key); // 猜猜会打印什么?
    }

    在不查看 touchFn 的代码的情况下,因为不确定它对 data 做了什么,你是不可能知道会打印什么。但如果 data 是 Immutable 的呢,你可以很肯定的知道打印的是 value。

Immutable 能够完整记录历史的状态,因此我们不用担心 Immutable 数据会在某些地方被改变。它就像一个“快照”,你只要保留它,就能在之后的任何地方访问到历史的记录。

  1. 节省内存 Immutable.js 使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。Structure Sharing 避免了深拷贝对内存的占用。
    
    import { Map} from 'immutable';
    let a = Map({
    select: 'users',
    filter: Map({ name: 'Cam' })
    })
    let b = a.set('select', 'people');

a === b; // false a.get('filter') === b.get('filter'); // true

上述代码中,`filter` 属性没有改变,因此复用了该节点下的对象。

3. 拥抱函数式编程
Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

### 缺点
1. 需要学习新的 API
2. 增加了资源文件大小
3. **容易与原生对象混淆**
这点是我们使用 Immutable.js 过程中遇到最大的问题。写代码要做思维上的转变。
虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。
Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 `map.get('key')` 而不是 `map.key`,`array.get(0)` 而不是 `array[0]`。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。
> 庆幸的是,ES6 之后,JS 对于对象和数组的使用,渐渐从命令式编程逐渐转向函数式编程,并且新增了 Map 和 Set 等新的数据结构。

如何避免“混淆”?
1. 使用 Flow 或 TypeScript 这类有静态类型检查的工具
2. 约定变量命名规则:如所有 Immutable 类型对象以 $$ 开头。
3. 使用 `Immutable.fromJS` 而不是 `Immutable.Map` 或 `Immutable.List` 来创建对象,这样可以避免 Immutable 和原生对象间的混用。
> 使用前通过 `Immutable.fromJS()` 将 JS 对象转为 Immutable 数据,返回数据时再通过 `Immutable.toJS()` 将数据转回 JS 对象。

## immutable 文档
参考 [Immutable-js](https://immutable-js.com/docs/v4.0.0-rc.13) 官方文档。
常用的方法如下:
1. Map()
```js
const { Map } = require('immutable'); 
// 将对象转为 Immutable Map 数据结构
const map1 = Map({ a: 1, b: 2, c: 3 }); 

// immutable-map 的常用方法, 类似于 js Map 结构: set, get, update ...
const map2 = map1.set('b', 50);
console.log(map1.get('b')); // 2
console.log(map2.get('b')); // 50
  1. List()
    
    const { List } = require('immutable');

// 将对象转为 Immutable List 数据结构'; const list1 = List([ 1, 2 ]); 

// Immutable List 的方法,类似于 js array const list2 = list1.push(3, 4, 5);  // [1,2,3,4,5] const list3 = list2.unshift(0);    // [0,1,2,3,4,5] const list4 = list1.concat(list2, list3); // [1,2,3,4,5,0,1,2,3,4,5] //push, set, unshift or splice 都可以直接用,返回一个新的immutable对象

3. merge() 连接对象 | concat() 连接数组
```js
// Immutable 不能使用扩展运算符,因此需要使用其规定的方法来实现拼接
const { Map, List } = require('immutable');

const map1 = Map({ a: 1, b: 2, c: 3, d: 4 });
const map2 = Map({ c: 10, a: 20, t: 30 });
const obj = { d: 100, o: 200, g: 300 }; // js object 也可以作为参数,其会先被转为 Immutable 再拼接
const map3 = map1.merge(map2, obj);// Map { a: 20, b: 2, c: 10, d: 100, t: 30, o: 200, g: 300 }

const list1 = List([ 1, 2, 3 ]);
const list2 = List([ 4, 5, 6 ]);
const array = [7, 8, 9];
const list3 = list1.concat(list2, array);// List [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
  1. fromJS() 包裹 js对象转换为immutable对象
    
    // fromJS 是 immutable 库的一个方法
    const { fromJS } = require('immutable');
    const nested = fromJS({ a: { b: { c: [ 3, 4, 5 ] } } });// Map { a: Map { b: Map { c: List [ 3, 4, 5 ] } } }

const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } });// Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 6 } } }

//如果取一级属性 直接通过get方法,如果取多级属性 getIn(["a","b","c"]]) console.log(nested2.getIn([ 'a', 'b', 'd' ])); // 6

// setIn 设置新的值const nested3 = nested2.setIn([ 'a', 'b', 'd' ], "kerwin");// Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: "kerwin" } } }

// updateIn 回调函数更新 const nested3 = nested2.updateIn([ 'a', 'b', 'd' ], value => value + 1); console.log(nested3);// Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 7 } } } const nested4 = nested3.updateIn([ 'a', 'b', 'c' ], list => list.push(6));// Map { a: Map { b: Map { c: List [ 3, 4, 5, 6 ], d: 7 } } }

5. toJS() 把immutable对象转换为js对象
```js
const { Map, List } = require('immutable');
const deep = Map({ a: 1, b: 2, c: List([ 3, 4, 5 ]) });

// 转为 object
console.log(deep.toObject());   // { a: 1, b: 2, c: List [ 3, 4, 5 ] }
// 转为 array
console.log(deep.toArray());    // [ 1, 2, List [ 3, 4, 5 ] ]
// 深层转换
console.log(deep.toJS());       // { a: 1, b: 2, c: [ 3, 4, 5 ] }
JSON.stringify(deep);           // '{"a":1,"b":2,"c":[3,4,5]}'

Redux + Immutable

未使用immutable时,一旦当newStateList中的类型较为复杂(包含引用类型),且需要修改newStateList时,就会发生报错,因为[...xxx, ...xxx]是浅拷贝,会影响原来的状态。 Redux 结合 Immutable 使用,通过store中传递过来的老状态prevState先转化为immutable对象,对深拷贝之后的对象,再进行修改等操作时,不会影响原状态,最后再通过toJS()转换为js对象即可。

import {fromJS} from 'immutable';

// 先将接受数据转为 Immutable 格式
export const reducer = (prevState = fromJS([]), action) {
  let {type, payload} = action;
  switch (type) {
    case 'AddList':
      let newStateList = prevState.concat(payload); // 需用 Immutable 内的数据操作方式操纵数据
      return newStateList.toJS(); // 返回 JS 数据格式
  } 
}