Shared mutable state is the root of all evil(共享的可变状态是万恶之源)前端
-- Pete Huntnode
immutable使用git
项目实践程序员
有人说 Immutable 能够给 React 应用带来数十倍的提高,github
也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,算法
由于同期 React 太火,它的光芒被掩盖了。数据库
这些至少说明 Immutable 是颇有价值的,下面咱们来一探究竟。编程
JavaScript 中的对象通常是可变的(Mutable),由于使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。Immutable 能够很好地解决这些问题。redux
Immutable Data 就是一旦建立,就不能再被更改的数据。数组
对 Immutable 对象的 任何修改 或 添加 删除操做 都会 返回一个新的 Immutable 对象。
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构), 也就是使用旧数据建立新数据时,要保证旧数据同时可用且不变。
同时为了不 deepCopy 把全部节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),
即若是对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:
请移步 http://img.alicdn.com/tps/i2/TB1zzi_KXXXXXctXFXXbrb8OVXX-613-575.gif 观看
目前流行的 Immutable 库有两个:
1. immutable.js
Facebook 工程师 Lee Byron 花费 3 年时间打造,与 React 同期出现,但没有被默认放到 React 工具集里(React 提供了简化的 Helper)。
它内部实现了一套完整的 Persistent Data Structure,还有不少易用的数据类型。
像 `Collection`、`List`、`Map`、`Set`、`Record`、`Seq`。有很是全面的`map`、`filter`、`groupBy`、`reduce``find`函数式操做方法。
同时 API 也尽可能与 Object 或 Array 相似。
其中有 3 种最重要的数据结构说明一下:(Java 程序员应该最熟悉了)
2. seamless-immutable
与 Immutable.js 学院派的风格不一样,seamless-immutable 并无实现完整的 Persistent Data Structure,
而是使用 `Object.defineProperty`(所以只能在 IE9 及以上使用)扩展了 JavaScript 的 Array 和 Object 对象来实现,
只支持 Array 和 Object 两种数据类型,API 基于与 Array 和 Object 操持不变。
代码库很是小,压缩后下载只有 2K。而 Immutable.js 压缩后下载有 16K。
下面上代码来感觉一下二者的不一样:
// 原来的写法 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';
foo = {a: {b: 1}};
foo = Immutable.fromJS (foo);
bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 赋值 改变a.b
console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 a.b的值 1
console.log(foo === bar); // 打印 false // 使用 seamless-immutable.js 后
import SImmutable from 'seamless-immutable';
foo = {a: {b: 1}}
foo = SImmutable(foo)
bar = foo.merge({a: { b: 2}}) // 使用 merge 赋值, bar.a.b 为 2
console.log(foo.a.b); // 像原生 Object 同样取值, foo.a.b 为 1 console.log(foo === bar); // 打印 false
1. Immutable 下降了 Mutable 带来的复杂度 能够回溯
可变(Mutable)数据耦合了 Time 和 Value 的概念,形成了数据很难被回溯。
好比下面一段代码:
function touchAndLog(touchFn) { let data = { key: 'value' }; touchFn(data); console.log(data.key); // 猜猜会打印什么?
//使用immutalbe
data = immutable(data)
tachFn(data)
data.key // value, data数据被固化了, 瞬间复杂度降低 }
在不查看 `touchFn` 的代码的状况下,由于不肯定它对 `data` 作了什么,你是不可能知道会打印什么(这不是废话吗)。
但若是 `data` 是 Immutable 的呢,你能够很确定的知道打印的是 `value`。
2. 节省内存
Immutable.js 使用了 Structure Sharing 会尽可能复用内存。没有被引用的对象会被垃圾回收。
import { Map } from 'immutable';
let a = Map({ select: 'users', filter: Map({ name: 'Cam' }) })
let b = a.set('select', 'people'); //? set方法 a === b; // false a.get('filter') === b.get('filter'); // true
上面 a 和 b 共享了没有变化的 `filter` 节点。
3. Undo/Redo,Copy/Paste, 甚至时间旅行这些功能作起来小菜一碟
由于每次数据都是不同的,只要把这些数据放到一个数组里储存起来,
想回退到哪里就拿出对应数据便可,很容易开发出撤销重作这种功能。
后面我会提供 Flux 作 Undo 的示例。
4. 并发安全
传统的并发很是难作,由于要处理各类数据不一致问题,所以『聪明人』发明了各类锁来解决。
但使用了 Immutable 以后,数据天生是不可变的,并发锁就不须要了。
然而如今并没什么卵用,由于 JavaScript 仍是单线程运行的啊。但将来可能会加入,提早解决将来的问题不也挺好吗?
5. 拥抱函数式编程
Immutable 自己就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。
由于只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。
像 ClojureScript,Elm 等函数式编程语言中的数据类型天生都是 Immutable 的,
这也是为何 ClojureScript 基于 React 的框架 --- Om 性能比 React 还要好的缘由。
1. 须要学习新的 API
No Comments
2. 增长了资源文件大小
No Comments
3. 容易与原生对象混淆
这点是咱们使用 Immutable.js 过程当中遇到最大的问题。写代码要作思惟上的转变。
虽然 Immutable.js 尽可能尝试把 API 设计的原生对象相似,
有的时候仍是很难区别究竟是 Immutable 对象仍是原生对象,容易混淆操做。
Immutable 中的 Map 和 List 虽对应原生 Object 和 Array,
但操做很是不一样,好比你要用 `map.get('key')` 而不是 `map.key`,`array.get(0)` 而不是 `array[0]`。
另外 Immutable 每次修改都会返回新对象,也很容易忘记赋值。
当使用外部库的时候,通常须要使用原生对象,也很容易忘记转换。
下面给出一些办法来避免相似问题发生:
两个 immutable 对象可使用 `===` 来比较,这样是直接比较内存地址,性能最好。
但即便两个对象的值是同样的,也会返回 `false`:
let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2; // false
为了直接比较对象的值,immutable.js 提供了 `Immutable.is` 来作『值比较』,结果以下:
Immutable.is(map1, map2); // true
`Immutable.is` 比较的是两个对象的 `hashCode` 或 `valueOf`(对于 JavaScript 对象)。
因为 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 `hashCode` 相等,值就是同样的。
这样的算法避免了深度遍历比较,性能很是好。
后面会使用 `Immutable.is` 来减小 React 重复渲染,提升性能。
另外,还有 mori、cortex 等,由于相似就再也不介绍。
2. 与 Object.freeze、const 区别
`Object.freeze` 和 ES6 中新加入的 `const` 均可以达到防止对象被篡改的功能, (const的属性是能够修改的)
但它们是 shallowCopy 的。对象层级一深就要特殊处理了。
3. Cursor 的概念
这个 Cursor 和 数据库中的游标是彻底不一样的概念。
因为 Immutable 数据通常嵌套很是深,为了便于访问深层数据, Cursor 提供了能够直接访问这个深层数据的引用。
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. 与 React 搭配使用,Pure Render
熟悉 React 的都知道,React 作性能优化时有一个避免重复渲染的大招,就是使用 `shouldComponentUpdate()`,
但它默认返回 `true`,即始终会执行 `render()` 方法,而后作 Virtual DOM 比较,并得出是否须要作真实 DOM 更新,
这里每每会带来不少无必要的渲染并成为性能瓶颈。
固然咱们也能够在 `shouldComponentUpdate()` 中使用使用 deepCopy 和 deepCompare 来避免无必要的 `render()`,
但 deepCopy 和 deepCompare 通常都是很是耗性能的。
Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 `===` 和 `is` 比较就能知道是否须要执行 `render()`, (引用不同 可是值同样)
而这个操做几乎 0 成本 ?,因此能够极大提升性能。修改后的 `shouldComponentUpdate` 是这样的:
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 (thisProps[key] !== nextProps[key] || !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; }
使用 Immutable 后,以下图,当红色节点的 state 变化后,不会再渲染树中的全部节点,而是只渲染图中绿色的部分:
你也能够借助 `React.addons.PureRenderMixin` 或支持 class 语法的pure-render-decorator来实现。
setState 的一个技巧
React 建议把 `this.state` 看成 Immutable 的,所以修改前须要作一个 deepCopy,显得麻烦:
import '_' from 'lodash'; const Component = React.createClass({ getInitialState() { return { data: { times: 0 } } }, handleAdd() { let data = _.cloneDeep(this.state.data);
data.times = data.times + 1; this.setState({ data: data });
// 若是上面不作 cloneDeep,下面打印的结果会是已经加 1 后的值。 ??说反了吧 console.log(this.state.data.times); } }
更正
上代码:
let data = this.state.data; data.times = data.times + 1; this.setState({ data: data }); // 下面打印的结果会是已经加 1 后的值。 由于是同一个引用, 因此不会更新 console.log(this.state.data.times);
由于上面的操做直接改变了this.state.data
的值,因此在shouldComponentUpdate
中nextState.data
和this.state.data
实际上是同一个对象
(2016-12-03更新:这里容易产生误区,再说得详细一点,由于咱们在调用this.setState({ data: data })
时,把原来的state中的state赋给了新的state,
因此下面的nextState.data
和this.state.data
是同一个对象,既然是同一个对象的两个不一样引用而已,
那么不管怎么比较得出的结果都是nextState.data
和this.state.data
相同,因此返回false
),所以不管怎么比较都会返回false
,致使组件不更新。
正确的作法应该以下:
let data = _.cloneDeep(this.state.data); data.times = data.times + 1; this.setState({ data: data }); // 上面作了 cloneDeep,下面打印的结果会是已经加 1 后的值。 console.log(this.state.data.times);
这样的话,没有改变this.state.data
的值,经过调用setState
,使得在shouldComponentUpdate
中nextState.data
是新的值,能够与this.state.data
比较,根据比较结果判断是否更新组件。
使用 Immutable 后:
getInitialState() { return { data: Map({ times: 0 }) } }, handleAdd() { this.setState({ data: this.state.data.update('times', v => v + 1) }); // 这时的 times 并不会改变 ?? 这样使用, 好像没什么意义 console.log(this.state.data.get('times')); }
上面的 `handleAdd` 能够简写成:
handleAdd() { this.setState(({data}) => ({ data: data.update('times', v => v + 1) }) }); }
2. 与 Flux 搭配使用
因为 Flux 并无限定 Store 中数据的类型,使用 Immutable 很是简单。
如今是实现一个相似带有添加和撤销功能的 Store:
import { Map, OrderedMap } from 'immutable';
let todos = OrderedMap(); let history = []; // 普通数组,存放每次操做后产生的数据 let TodoStore = createStore({ getAll() { return todos; } }); Dispatcher.register(action => { if (action.actionType === 'create') { let id = createGUID(); history.push(todos); // 记录当前操做前的数据,便于撤销 todos = todos.set(id, Map({ id: id, complete: false, text: action.text.trim() })); TodoStore.emitChange(); } else if (action.actionType === 'undo') { // 这里是撤销功能实现, // 只需从 history 数组中取前一次 todos 便可 if (history.length > 0) { todos = history.pop(); } TodoStore.emitChange(); } });
3. 与 Redux 搭配使用
Redux 是目前流行的 Flux 衍生库。它简化了 Flux 中多个 Store 的概念,只有一个 Store,
数据操做经过 Reducer 中实现; 同时它提供更简洁和清晰的单向数据流(View -> Action -> Middleware -> Reducer),
也更易于开发同构应用。目前已经在咱们项目中大规模使用。
因为 Redux 中内置的 `combineReducers` 和 reducer 中的 `initialState` 都为原生的 Object 对象,因此不能和 Immutable 原生搭配使用。
幸运的是,Redux 并不排斥使用 Immutable,能够本身重写 `combineReducers` 或使用 redux-immutablejs 来提供支持。
上面咱们提到 Cursor 能够方便检索和 update 层级比较深的数据,但由于 Redux 中已经有了 select 来作检索,Action 来更新数据,所以 Cursor 在这里就没有用武之地了。
Immutable 能够给应用带来极大的性能提高,可是否使用还要看项目状况。
因为侵入性较强,新项目引入比较容易,老项目迁移须要评估迁移。
对于一些提供给外部使用的公共组件,最好不要把 Immutable 对象直接暴露在对外接口中。
若是 JS 原生 Immutable 类型会不会太美,被称为 React API 终结者的 Sebastian Markbåge 有一个这样的提案,可否经过如今还不肯定。
不过能够确定的是 Immutable 会被愈来愈多的项目使用。
码这么多字不容易,喜欢就给个 赞 吧,亲