概述
为什么要了解不可变数据?
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 中的扩展运算符也可以比较方便的实现数据的不可变。
- 添加
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']
- 删除
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']
- 更新
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