Immutable.js & React

912 阅读6分钟

概述

为什么要了解不可变数据?

Redux 要求全局状态具有不可变性。

Redux 要求本地组件状态具有不可变性。

数据突变与不可变

什么是数据突变

const p1 = { name: '张三' };
const p2 = p1;
p2.name = '李四';
console.log(p1.name); // '李四'
console.log(p2.name); // '李四'
const a = [2, 1, 4, 3];
const b = a.sort();
console.log(b); // [1, 2, 3, 4]
console.log(a); // [1, 2, 3, 4]

在 JavaScript 中,对象属于引用数据类型,将一个对象赋值给两一个对象时,实际上是将对象的引用地址赋值给了另一个对象,此时两个变量同时指向了内存中的同一个对象,通过两个变量对对象进行的任何操作,都会影响另一方,这就是数据突变。

由于数据突变带来的不可预测,非常容易导致改 A 坏 B 的问题。

什么是数据的不可变

对引用类型的数据进行更改,更改并不会作用于原数据,而是返回一个更改后的全新的数据。

const before = ['a', 'b', 'c', 'd'];
const after = before.slice(0, 2);
console.log(after); // ['a', 'b']
console.log(before); // ['a', 'b', 'c', 'd']

可以把数据突变想象为“保存”, 它会在原有数据上进行修改。 可以把数据的不可变想象为“另存为”,它会返回全新的更改后的数据。由于数据的不可变,使数据操作更加安全,更加可预测。

JavaScript 中的数据不可变

在 JavaScript 中,既提供了数据突变方法,又提供了数据不可变方法。

sort、splice、push、pop 等就属于数据突变方法。

map、filter、reduce、slice 等就属于数据不可变方法。

JavaScript 中的扩展运算符也可以比较方便的实现数据的不可变。

  1. 添加
const before = ['a', 'b', 'c', 'd'];
const after = [...before, 'e'];
console.log(after); // ['a', 'b', 'c', 'd', 'e']
console.log(before); // ['a', 'b', 'c', 'd']
  1. 删除
const before = ['a', 'b', 'c', 'd'];
const after = [...before.slice(0, 2), ...before.slice(3)];
console.log(after); // ['a', 'b', 'd' ]
console.log(before); // ['a', 'b', 'c', 'd']
  1. 更新
const before = ['a', 'b', 'c', 'd'];
const after = [...before.slice(0, 1), 'x', 'y', ...before.slice(3)];
console.log(after); // ['a', 'x', 'y', 'd' ]
console.log(before); // ['a', 'b', 'c', 'd']

不完整的数据不可变

JavaScript 不具备完整的数据不可变性,因为它提供的那些具有数据不可变的方法都属于浅拷贝,对于引用数据类型嵌套的情况,内层数据仍然是引用地址的拷贝。

const state = [{ name: 'super me' }];
const newState = state.slice(0);
newState[0].name = '李四';

console.log(newState); // [{ name: '李四'}]
console.log(state); // [{ name: '李四'}]

以上问题可以通过深拷贝解决,但是深拷贝是有性能问题的,其一是每次深拷贝都要把整个对象递归的复制一份,递归的过程是消耗性能的,其二是在内存中多出了很多重复的相同的数据,占用内存。

const p1 = { name: '张三', skill: ['编程', '驾驶'] };
const p2 = deepClone(p1);
p2.name = '李四';

比如上例中将p1进行了深拷贝,p1和p2就变成了两个完全独立的对象,虽然解决了 name 属性值互不影响的问题,但是同时在内存中也多出了一份完全相同的skill属性。

理想状态应该是两个对象中name属性是独立的,skill属性是共享的。

const skill = ['编程', '驾驶'];
const p1 = { name: '张三', skill };
const p1 = { name: '李四', skill };

Immutable.js

概述

Immutable.js 中提供了不可变数据结构,主要解决两个问题,第一是防止数据突变,第二是提升数据操作性能。

防止数据突变

Immutable 意味不可变数据,每次操作都会产生一个新的不可变数据,无论这个操作是增加,删除还是修改,都不会影响到原有的不可变数据。不可变数据可以防止数据突变带来的不可预测性。

提升数据操作性能

不可变数据采用了数据结构共享,返回的新的不可变数据中,发生变化的数据是独立的,其他没有发生变化的数据是共享的,数据结构共享解决了深拷贝带来的性能问题。

下载:npm install immutable@4.0.0-rc.12

官网:https://immutable-js.github.io/immutable-js

数据结构

在 Immutable.js 中提供了多种数据结构用于实现不可变数据,常用的有两种,即 List 和 Map。

List 对应 JavaScript 中的数组。

Map 对应 JavaScript 中的对象。

import { List, Map } from 'immutable';

const l1 = new List(['a', 'b']);
console.log(l1); // List ['a', 'b']
const m1 = new Map({ a: 1, b: 2 });
console.log(m1); // Map { a: 1, b: 2 }

实例方法

设置数据

import { List } from 'immutable';

const l1 = new List(['a', 'b']);
const l2 = l1.set(0, "x");
console.log(l2); // List ['x', 'b']
console.log(l1); // List ['a', 'b']
import { Map } from 'immutable';

const m1 = new Map({ a: 1, b: 2 });
const m2 = m1.set('a', 100);
console.log(m2); // Map { a: 100, b: 2 }
console.log(l\m1); // Map { a: 1, b: 2 }

获取数据

import  { List } from 'immutable';

const l1 = new List(['a', 'b']);
console.log(l1.get(0)); // 'a';
import { Map } from 'immutable';

const m1 = new Map({ a: 1, b: 2 });
console.log(m1.get('a')); // 1;

合并数据

import  { List } from 'immutable';

const l1 = new List(['a', 'b']);
const l2 = new List(['c', 'd']);
const l3 = l1.merge(l2);

console.log(l3); // List ['a', 'b', 'c', 'd']
console.log(l1); // List ['a', 'b']
console.log(l2); // List ['c', 'd']
import { Map } from 'immutable';

const m1 = new Map({ a: 1, b: 2 });
const m2 = new Map({ c: 3, d: 4 });
const m3 = m1.merge(m2);
console.log(m3); // Map { a: 1, b: 2, c: 3, d: 4 }
console.log(m1); // Map { a: 1, b: 2 }
console.log(m2); // Map { c: 3, d: 4 }

删除数据

import  { List } from 'immutable';

const l1 = new List(['a', 'b']);
const l2 = l1.remove(0);
console.log(l2); // List ['b']
console.log(l1); // List ['a', 'b']
import { Map } from 'immutable';

const m1 = new Map({ a: 1, b: 2 });
const m2 = m1.remove('a');
console.log(m2); // Map { b: 2 }
console.log(m1); // Map { a: 1, b: 2 }

更新数据

import  { List } from 'immutable';

const l1 = new List(['a', 'b']);
const l2 = l1.update(0, target => target + 'hello');
console.log(l2); // List ['ahello', 'b']
console.log(l1); // List ['a', 'b']
import { Map } from 'immutable';

const m1 = new Map({ a: 1, b: 2 });
const m2 = m1.update('b', target => target * 2);
console.log(m2); // Map { a: 1, b: 4 }
console.log(m1); // Map { a: 1, b: 2 }

数据类型转换

使用 fromJS 方法将数组和对象转换为不可变数据,数组转为 List,对象转为 Map。Map 和 List 方法在创建数据时不支持深层嵌套,fromJS 方法支持深层嵌套。

import { Map } from 'immutable';

const m1 = new Map({ a: { b: { c: 1 } } });
console.log(m1) // Map {'a': [object Object]} 
import { fromJS } from 'immutable';

const f1 = fromJS({ a: { b: { c: 1 } } });
console.log(f1) // Map {'a': Map {b: Map {c: 1}}} 
import { fromJS } from 'immutable';

const f1 = fromJS({ a: { b: { c: 1 } } });
const f2 = f1.setIn(['a', 'b', 'c'], 100);
console.log(f2) // Map {'a': Map {b: Map {c: 100}}}
console.log(f1) // Map {'a': Map {b: Map {c: 1}}} 

数据比较

使用 is 方法判断两个不可变数据是否相同。

import { fromJS, is } from 'immutable';

const m1 = new Map({ a: { b: { c: 1 } } });
const m2 = new Map({ a: { b: { c: 1 } } });

console.log(is(m1, m2)) // true

Immutable.js & React

性能优化

在 React 中,当调用 setState 方法更新数据时,即使传入的数据和以前一样,React 也会执行 diff 的过程,因为 JavaScript 中对象与对象的比较采用的是引用地址,所以即使两个对象长得一样,其实也是不相等的,所以会走 diff 的过程。为了解决这个问题,React 提供了 PureComponent,但是 PureComponent 采用的是浅层比较,当数据结构比较复杂时,依然会存在无效的 diff 操作。

immutable.js 提供了数据结构共享特性,能够快速进行差异比较,使组件更加智能的渲染。

import { fromJS, is } from 'immutable';
import { Component } from 'react';

class APP extends Component {
  constructor() {
    super()
    this.state = {
      person: fromJS({
        name: '张三'
      })
    }
  }
}

shouldComponentUpdate(nextProps, nextState) {
  if (!is(this.state.person, nextState.person)) {
    return true;
  }
  return false;
}

render() {
  console.log('render')
  return (
    <div>
      <p>{this.state.person.get('name')}</p>
      <button
        onClick={() => this.setState({
          person: this.state.person.set('name', '张三')
        })}
      >
        按钮
      </button>
    </div>
  )
}

export default App

防止数据突变

import { fromJS, is } from 'immutable';
import { Component } from 'react';

class APP extends Component {
  constructor() {
    super()
    this.state = {
      person: fromJS({
        name: '张三',
        age: 20
      })
    }
  }
}

render() {
  console.log('render')
  return (
    <div>
      <p>{this.state.person.get('name')}</p>
      <p>{this.state.person.get('age')}</p>
      <button
        onClick={() => this.setState({
          person: this.state.person.set('name', '李四')
        })}
      >
        按钮
      </button>
    </div>
  )
}

export default App

以上例子如果直接this.setState({person: {name: '李四'}})会发生数据突变。

Immutable & Redux

import ReactDOM from 'react-dom';
import APP from './App';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { fromJS } from 'immutable';

const initialState = fromJS({ count: 0 })

function reducer (state = initialState, action) {
  let count = state.get('count')
  switch (action.type) {
    case 'increment':
      return state.set('count', count + 1)
    case 'decrement':
      return state.set('count', count - 1)
    default: 
      return state;
  }
}

const store = createStore(reducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
import { useDispatch, useSelector } from 'react-redux';

function App () {
  const dispatch = useDispatch();
  const count = useSelector(state => state.get('count')
  return (
    <div>
      {count}
      <button onClick={() => dispatch({type: 'increment'})}>
        increment
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>
        decrement
      </button>
    </div>
  )
}

export default App