2023-09-05
ReactJS
00
请注意,本文编写于 325 天前,最后修改于 325 天前,其中某些信息可能已经过时。

目录

背景: 为什会有immutable?
什么是immutable?
immutable.js
快速入门API
Map类型
不可变类型比较
JavaScript优先的API
嵌套结构
批处理突变
惰性序列
cursor
优点
缺点
使用immutable.js优化react
immer
概念说明
使用immer优化react
reduce中使用immer

本文介绍不可变数据类型ImmutableJSImmerJS的基本用法,以及它们在React项目中的使用。

背景: 为什会有immutable?

在JavaScript中有引用类型和基本类型。

  • 如果 变量a是基本类型,将a赋值给变量b,再修改b的值,则变量a不受影响
  • 如果 变量a是引用类型,将a赋值给变量b, 则b与a是同一个对象的引用,若修改b对象的属性,a对象的该属性也会跟着变化。

    解决方案就是深拷贝,比如最简单的方式JSON.parse(JSON.stringify(obj))
  • 但是深拷贝这种方式比较耗费性能(空间和时间),有没有折中的方案呢? 即修改b的同时不影响a,且比深拷贝节省性能。
  • 所以有了immutable.js,简单说就是按需深拷贝。
  • 此外还有一个重要原因,开发中经常遇到函数带有副作用,副作用的函数修改了一个引用类型某个属性的值或者浅copy的问题,immutable为此类问题提供了一种优雅的解决方案。

什么是immutable?

  • Immutable Data 就是一旦创建,就不能再被更改的数据
  • Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象;
  • Immutable 实现的原理是 Persistent Data Structure持久化数据结构):也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

immutable.js

immutable是Javascript中不可变的集合
官网

Facebook 工程师 Lee Byron 花费 3 年时间打造,与 React 同期出现,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。它内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、ListMapSetRecordSeq。有非常全面的mapfiltergroupByreducefind函数式操作方法。同时 API 也尽量与 ObjectArray 类似。

javascript
// 原来的写法 let foo = {a: {b: 1}}; let bar = foo; bar.a.b = 2; console.log(foo.a.b); // 打印 2 console.log(foo === bar); // 打印 true // 使用 immutable.js 后 import Immutable from 'immutable'; var foo = Immutable.fromJS({a: {b: 1}}); var bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 赋值 console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1 // 等价于 foo.getIn(['a']).getIn(['b']) console.log(foo === bar); // 打印 false

快速入门API

笔者没有找关于immutable相关能够通俗易懂快速上手的中文文档,无奈只能自己阅读官方文档,以下内容是笔者基于官方文档整理的自己的总结

javascript
// 前面提到为了消除引用类型及浅拷贝的副作用,引入了immutable概念 // 但是深拷贝这种方式比较耗费性能 // 因此寻找一种既能避免副作用又节省开销的方式--结构共享 // 没有immutable.js我们可能采用下面的方式避免副作用 var a = {b:'B',c:{d:1}}; var b = Object.assign({}, a); b.b = 'new val bbb'; console.log(b.b) // 'new val bbb' console.log(a.b); // 仍旧为 'B' console.log(a.c === b.c); // true // 显然 7,8行代码书写起来不方面, // 如果a.b也是引用类型,还对a.b 重复进行上述两部操作

然而 immutable.js 的使用有一定的上手成本,它自定义了一些数据结构

Map类型

javascript
// 如果是对象类型,在immutable中使用 Map const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: [1,2] }); // 修改原对象的属性会产生一个新对象, 原对象保持不变 const map2 = map1.set('b', 50); // 使用 .equals 方法比较 console.log(map1.equals(map2)); // false // 取值 .get console.log(map1.get('b'), map2.get('b')); // 2, 50 console.log(map1.get('c') === map2.get('c')); // true, c没有改变

不可变类型比较

引用类型的比较是基于引用地址,而不可变对象的比较是基于集合的值是否一一相同

javascript
const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: 3 }); const map2 = Map({ a: 1, b: 2, c: 3 }); map1.equals(map2); // true // Immutable.is(map1, map2); // true map1 === map2; // false const map3 = map2.set('b', 2); map3 === map2; // true // 如果一个对象是不可变的, // 则可以简单地通过对它进行另一个引用而不是复制整个对象来“复制”它。 // 因为这可以节省内存并潜在地提高依赖副本的程序的执行速度(例如撤消堆栈)。

补充: 不可变对象的比较是基于值,算法复杂度O(N)O(N), 而引用类型比较算法复杂度 O(1)O(1) , 因此使用时要考虑性能权衡。

JavaScript优先的API

Immutable.js 具有一个与 ES2015 Array、Map 和 Set 非常相似的面向对象的 API。

JavaScriptimmutable
MapMap
SetSet
ArrayList
ObjectfromJS or Map
javascript
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 }; 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); // 从上面的示例可以看出 // 1. 与JS API靠拢 如 Array.prototype.concat // 2. api可以与JS对象互相操作 // 所有 Immutable.js 集合都是可迭代的 const { List } = require('immutable'); const aList = List([1, 2, 3]); const anArray = [0, ...aList, 4, 5]; // [ 0, 1, 2, 3, 4, 5 ]

嵌套结构

javascript
// 嵌套结构 const { fromJS } = require('immutable'); const nested = fromJS({ a: { b: { c: [3, 4, 5] } } }); console.log(typeof nested.toJS()); // 'object' const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } }); // nested2 { a: { b: { c: [3, 4, 5] }, d: 6 } } console.log(nested2.getIn(['a', 'b', 'd'])); // 6 console.log(nested2.getIn(['a']).getIn(['b', 'd'])); // 6 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 } } }

批处理突变

默认情况下immutable.js中的api每执行一次就会返回一个新的immutable对象
那么如果我只需要最后生成的immutable对象,中间的对象都不要,可不可以只返回新一个新对象呢?
这就用到了 withMutations ,批量处理节省开销,目前只有少数方法 setpushpop 可直接应用于持久化数据结构

javascript
const { List } = require('immutable'); const list1 = List([1, 2, 3]); const list2 = list1.withMutations(function (list) { list.push(4).push(5).push(6); }); console.log(list1.size); // 3 console.log(list2.size); // 6

惰性序列

javascript
const { Seq } = require('immutable'); const oddSquares = Seq([1, 2, 3, 4, 5, 6, 7, 8]) .filter(x => { console.log('filter', x) return x % 2 !== 0 }) .map(x => x * x); // 因为oddSquares是一个immutable对象,是惰性执行的,上述代码不会做任何操作 // 当把immutable对象转换成 JavaScript 对象是 才会执行 console.log(oddSquares.toJS()); // 控制台打印了 ‘filter’ // Range 是一种特殊的 Lazy 序列。 const { Range } = require('immutable'); const a = Range(990, 1010) // .skip(100) .map(n => { console.log(n) return -n; }) .reduce((r, n) => r * n, 1); console.log('---', a) // --- 9.897178826145609e+59

cursor

由于 Immutable 数据一般嵌套非常深,为了便于访问深层数据,Cursor 提供了可以直接访问这个深层数据的引用。

javascript
import Immutable from 'immutable'; import Cursor from 'immutable/contrib/cursor'; let data = Immutable.fromJS({ a: { b: { c: 1 } } }); // 让 cursor 指向 { c: 1 } let cursor = Cursor.from(data, ['a', 'b'], newData => { // 当 cursor 或其子 cursor 执行 update 时调用 console.log(newData); }); cursor.get('c'); // 1 cursor = cursor.update('c', x => x + 1); cursor.get('c'); // 2

优点

  1. 降低了mutable带来的复杂性
javascript
function touchAndLog(touchFn) { let data = { key: 'value' }; touchFn(data); console.log(data.key); // 因为不知道touchFn进行了什么操作,所以无法预料,但使用immutable,肯定是value }
  1. 节省内存 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。
javascript
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
  1. Undo/RedoCopy/Paste
    因为每次数据都是不一样的,所有可以存储在数组里,想回退到哪里就拿出对应数据即可

缺点

  1. 需要学习新的API
  2. 容易与原生对象混淆
    1. 虽然 Immutable.js 尽量尝试把 API 设计的原生对象类似,有的时候还是很难区别到底是 Immutable 对象还是原生对象,容易混淆操作。
    2. Immutable 中的 MapList 虽对应原生 ObjectArray,但操作非常不同,比如你要用 map.get('key') 而不是 map.keyarray.get(0) 而不是 array[0]。另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值;
    3. 当使用外部库的时候,一般需要使用原生对象,也很容易忘记转换。

下面给出一些办法来避免类似问题发生:

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

使用immutable.js优化react

  1. React可以使用 shouldComponentUpdate()进行性能优化,但它默认返回 true,即始终会执行 render() 方法,然后做 Virtual DOM 比较,并得出是否需要做真实 DOM 更新;
  2. 可以在shouldComponentUpdate 周期里执行deepCopydeepCompare 避免无意义的render,但deepFn也很耗时;
javascript
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; }

immer

官方文档

对于处理修改引用类型副作用问题,ImmutableJS 有两个较大的不足:

  1. 需要使用者学习它的数据结构操作方式,没有 Immer 提供的使用原生对象的操作方式简单、易用;
  2. 它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象的时候,时刻要知道操作的是原生对象还是 ImmutableJS 的返回结果,稍不注意,就会产生问题;
javascript
let currentState = { x:[2] } let o1 = currentState; o1.a = 1; let o2 = { ...currentState }; o2.x.push(3); console.log(currentState) // { x: [ 2, 3 ], a: 1 } /* 使用immer解决上述问题 */ import produce from 'immer'; let state = { x:[2] } let obj1 = produce(state, draft => { draft.a = 1; }); let obj2 = produce(state, draft => { draft.x.push(3); }); // { x: [ 2 ] } { x: [ 2 ], a: 1 } { x: [ 2, 3 ] } console.log(state, obj1, obj2); // produce方法做了哪些事情? // 遍历原始对象,依次冻各个属性。

概念说明

produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState

  • currentState:被操作对象的最初状态
  • draftState:根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的任何修改都将被记录并用于生成 nextState 。在此过程中,currentState 将不受影响
  • nextState:根据 draftState 生成的最终状态
  • produce:用来生成 nextStateproducer 的函数
  • producer:通过 produce 生成,用来生产 nextState ,每次执行相同的操作
  • recipe:用来操作 draftState 的函数

使用immer优化react

javascript
// 原写法 setState const { members } = this.state; this.setState({ members: [ { ...members[0], age: members[0].age + 1, }, ...members.slice(1), ] }) // 现在写法 const { members } = this.state; this.setState(this.setState(produce(members, draft => { draft.members[0].age++; })))

reduce中使用immer

javascript
// produce 内的 recipe 回调函数的第2个参数与obj对象是指向同一块内存 function reducer (state = {name: '章三', age: 12}, action) { return immer.produce(state, (draft) => { switch(action.type) { case 'ADD': draft.age++; return default: return; } }) } const store = Redux.createStore(reducer); class App extends React.Component { componentDidMount() { store.subscribe(() => { this.forceUpdate(); }) } render() { const state = store.getState(); return <div> <p>{state.name}今年{state.age}周岁</p> <button onClick={() => store.dispatch({type: 'ADD'})}>过生日</button> </div> } } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App/>);

完整案例 redux-immer.html

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!