Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React - Immutable 数据流 #15

Open
jtwang7 opened this issue Jul 6, 2021 · 0 comments
Open

React - Immutable 数据流 #15

jtwang7 opened this issue Jul 6, 2021 · 0 comments
Labels

Comments

@jtwang7
Copy link
Owner

jtwang7 commented Jul 6, 2021

参考文章:
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 属性没有改变,因此复用了该节点下的对象。

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

缺点

  1. 需要学习新的 API
  2. 增加了资源文件大小
  3. 容易与原生对象混淆
    这点是我们使用 Immutable.js 过程中遇到最大的问题。写代码要做思维上的转变。
    虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。
    Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,但操作非常不同,比如你要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。

庆幸的是,ES6 之后,JS 对于对象和数组的使用,渐渐从命令式编程逐渐转向函数式编程,并且新增了 Map 和 Set 等新的数据结构。

如何避免“混淆”?

  1. 使用 Flow 或 TypeScript 这类有静态类型检查的工具
  2. 约定变量命名规则:如所有 Immutable 类型对象以 $$ 开头。
  3. 使用 Immutable.fromJS 而不是 Immutable.MapImmutable.List 来创建对象,这样可以避免 Immutable 和原生对象间的混用。

使用前通过 Immutable.fromJS() 将 JS 对象转为 Immutable 数据,返回数据时再通过 Immutable.toJS() 将数据转回 JS 对象。

immutable 文档

参考 Immutable-js 官方文档。
常用的方法如下:

  1. Map()
const { Map } = require('immutable'); 
// 将对象转为 Immutable Map 数据结构
const map1 = Map({ a1, b2, c3 }); 

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

const map1 = Map({ a1, b2, c3, d4 });
const map2 = Map({ c10, a20, t30 });
const obj = { d100, o200, g300 }; // 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{ d6 } } });// 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 } } }
  1. toJS() 把immutable对象转换为js对象
const { Map, List } = require('immutable');
const deep = Map({ a1, b2, cList([ 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 数据格式
  } 
}
@jtwang7 jtwang7 added the React label Jul 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant