这是第 104 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:15 分钟学会 Immutable
1. 什么是 Immutable?
Immutable Data 就是一旦创立,就不能再被更改的数据。对 Immutable 对象的任何批改或增加删除操作都会返回一个新的 Immutable 对象。次要原理是采纳了 Persistent Data Structure(长久化数据结构 ),就是当每次批改后咱们都会失去一个新的版本,且旧版本能够完整保留,也就是应用旧数据创立新数据时,要保障旧数据同时可用且不变。同时为了防止 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 应用了 Structural Sharing(构造共享),就是对于本次操作没有批改的局部,咱们能够间接把相应的旧的节点拷贝过来,这其实就是构造共享。
2. Immutable 有什么长处?
2.1 升高复杂度,防止副作用
在 Javascript 中,对象都是援用类型,在按援用传递数据的场景中,会存在多个变量指向同一个内存地址的状况,这样会引发不可控的副作用,如下代码所示:
let obj1 = {name: '张三'};
let obj2 = obj1;
obj2.name = '李四';
console.log(obj1.name); // 李四
应用 Immutable 后:
import {Map} from 'immutable';
let obj1 = Map({name: '张三'});
let obj2 = obj1;
obj2.set({name:'李四'});
console.log(obj1.get('name')); // 张三
当咱们应用 Immutable 升高了 Javascript 对象 带来的复杂度的问题,使咱们状态变成可预测的。
2.2 节俭内存
Immutable 采纳了构造共享机制,所以会尽量复用内存。
import {Map} from 'immutable';
let obj1 = Map({
name: 'zcy',
filter: Map({age:6})
});
let obj2 = obj1.set('name','zcygov');
console.log(obj1.get('filter') === obj2.get('filter')); // true
// 下面 obj1 和 obj2 共享了没有变动的 filter 属性
2.3 不便回溯
Immutable 每次批改都会创立一个新对象,且对象不变,那么变更的记录就可能被保留下来,利用的状态变得可控、可追溯,不便撤销和重做性能的实现,请看上面代码示例:
import {Map} from 'immutable';
let historyIndex = 0;
let history = [Map({ name: 'zcy'})];
function operation(fn) {history = history.slice(0, historyIndex + 1);
let newVersion = fn(history[historyIndex]);
// 将新版本追加到历史列表中
history.push(newVersion);
// 记录索引,historyIndex 决定咱们是否有撤销和重做
historyIndex++;
}
function changeHeight(height) {operation(function(data) {return data.set('height', height);
});
}
// 判断是否有重做
let hasRedo = historyIndex !== history.length - 1;
// 判断是否有撤销
let hasUndo = historyIndex !== 0;
2.4 函数式编程
Immutable 自身就是函数式编程中的概念,纯函数式编程比面向对象更实用于前端开发。因为只有输出统一,输入必然统一,这样开发的组件更易于调试和组装。
2.5 丰盛的 API
Immutable 实现了一套残缺的 Persistent Data Structure,提供了很多易用的数据类型。像 Collection
、List
、Map
、Set
、Record
、Seq
,以及一系列操作它们的办法,包含 sort,filter,数据分组,reverse,flatten 以及创立子集等办法,具体 API 请参考官网文档
3. 在 react 中如何应用 Immutable
咱们都晓得在 React 父组件更新会引起子组件从新 render,当咱们传入组件的 props 和 state 只有一层时,咱们能够间接应用 React.PureComponent,它会主动帮咱们进行浅比拟,从而管制 shouldComponentUpdate 的返回值。
然而,当传入 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,浅比拟就生效了。当然咱们也能够在 shouldComponentUpdate()
中应用应用 deepCopy
和 deepCompare
来防止不必要的 render()
,但 deepCopy
和 deepCompare
个别都是十分耗性能的。这个时候咱们就须要 Immutable
。
以下示例通过浅比拟的形式来优化:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {state = { counter: { number: 0} }
handleClick = () => {let amount = this.amount.value ? Number(this.amount.value) : 0;
this.state.counter.number = this.state.counter.number + amount;
this.setState(this.state);
}
// 通过浅比拟判断是否须要刷新组件
// 浅比拟要求每次批改的时候都通过深度克隆每次都产生一个新对象
shouldComponentUpdate(nextProps, nextState) {for (const key in nextState) {if (this.State[key] !== nextState[key]) {return true;}
}
return false;
}
}
render() {console.log('render');
return (
<div>
<p>{this.state.number}</p>
<input ref={input => this.amount = input} />
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(
<Caculator />,
document.getElementById('root')
)
也能够通过深度比拟的形式判断两个状态的值是否相等这样做的话性能非常低。
shouldComponentUpdate(nextProps, prevState) {
// 通过 lodash 中 isEqual 深度比拟办法判断两个值是否雷同
return !_.isEqual(prevState, this.state);
}
Immutable 则提供了简洁高效的判断数据是否变动的办法,只需 ===
和 is
比拟就能晓得是否须要执行 render()
,而这个操作简直 0 老本,所以能够极大进步性能。批改后的 shouldComponentUpdate
是这样的:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {is, Map} from 'immutable';
class Caculator extends Component {
state = {counter: Map({ number: 0})
}
handleClick = () => {let amount = this.amount.value ? Number(this.amount.value) : 0;
let counter = this.state.counter.update('number', val => val + amount);
this.setState({counter});
}
shouldComponentUpdate(nextProps = {}, nextState = {}) {if (Object.keys(this.state).length !== Object.keys(nextState).length) {return true;}
// 应用 immutable.is 来进行两个对象的比拟
for (const key in nextState) {if (!is(this.state[key], nextState[key])) {return true;}
}
return false;
}
render() {
return (
<div>
<p>{this.state.counter.get('number')}</p>
<input ref={input => this.amount = input} />
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(
<Caculator />,
document.getElementById('root')
)
Immutable.is
比拟的是两个对象的 hashCode
或 valueOf
(对于 JavaScript 对象)。因为 immutable 外部应用了 Trie 数据结构来存储,只有两个对象的 hashCode
相等,值就是一样的。这样的算法防止了深度遍历比拟,性能十分好。
应用 Immutable 后,如下图,当红色节点的 state 变动后,不会再渲染树中的所有节点,而是只渲染图中绿色的局部:
(图片援用自:Immutable 详解及 React 中实际)
所以应用 Immutable.is
能够缩小 React 反复渲染,进步性能。
4. Immutable 联合 Redux 应用
上面是 Immutable 联合 Redux 应用的一个数值累加小示例:
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'
import {createStore, applyMiddleware} from 'redux'
import {Provider, connect} from 'react-redux'
import immutable, {is, Map} from 'immutable';
import PureComponent from './PureComponent';
const ADD = 'ADD';
// 初始化数据时,应用 Map 保证数据不会被轻易批改
const initState = Map({number: 0});
function counter(state = initState, action) {switch (action.type) {
case ADD:
// 返回数据时采纳 update 更新对象数据
return state.update('number', (value) => value + action.payload);
default:
return state
}
}
const store = createStore(counter);
class Caculator extends PureComponent {render() {
return (
<div>
<p>{this.props.number}</p>
<input ref={input => this.amount = input} />
<button onClick={() => this.props.add(this.amount.value ? Number(this.amount.value) : 0)}>+</button>
</div>
)
}
}
let actions = {add(payload) {return { type: ADD, payload}
}
}
const ConnectedCaculator = connect(state => ({ number: state.get('number') }),
actions
)(Caculator)
ReactDOM.render(<Provider store={store}><ConnectedCaculator /></Provider>,
document.getElementById('root')
)
但因为 Redux 中内置的 combineReducers
和 reducer 中的 initialState
都为原生的 Object 对象,所以不能和 Immutable 原生搭配应用,当然咱们能够通过重写 combineReducers
的形式达到兼容成果,如下代码所示:
// 重写 redux 中的 combineReducers
function combineReducers(reducers) {
// initialState 初始化为一个 Immutable Map 对象
return function (state = Map(), action) {let newState = Map();
for (let key in reducers) {newState = newState.set(key, reducers[key](state.get(key), action));
}
return newState;
}
}
let reducers = combineReducers({counter});
const ConnectedCaculator = connect(
state => {return ({ number: state.getIn(['counter', 'number']) })
},
actions
)(Caculator)
也能够通过引入 redux-immutable 中间件的形式实现 redux 与 Immutable 的搭配应用,对于应用 Redux 的应用程序来说,你的整个 state tree 应该是 Immutable.JS 对象,基本不须要应用一般的 JavaScript 对象。
5. 应用 Immutable 须要留神的点
- 不要混合一般的 JS 对象和 Immutable 对象(不要把 Imuutable 对象作为 Js 对象的属性,或者反过来)。
- 把整颗 Reudx 的 state 树作为 Immutable 对象。
- 除了展现组件以外,其余中央都应该应用 Immutable 对象(提高效率,而展现组件是纯组件,不应该应用)。
- 少用 toJS 办法(这个办法操作十分耗性能,它会深度遍历数据转换成 JS 对象)。
- 你的 Selector 应该永远返回 Immutable 对象(即 mapStateToProps,因为 react-redux 中是通过浅比拟来决定是否 re-redering,而应用 toJs 的话,每次都会返回一个新对象,即援用不同)。
6. 总结
理论状况中有很多办法能够优化咱们的 React 利用,例如提早加载组件,应用 serviceWorks 缓存利用状态,应用 SSR 等,但在思考优化之前,最好先了解 React 组件的工作原理,理解 Diff 算法,明确这些概念之后能力更好的针对性的去优化咱们的利用。
文章中如有不对的中央,欢送斧正。
参考链接:
Immutable 详解及 React 中实际
Immutable Data Structures and JavaScript
Immutable 官网文档
开源作品
- 政采云前端小报
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交换群)
招贤纳士
政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。
如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com