15 分钟学会 Immutable

这是第 104 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注咱们吧~ 本文首发于政采云前端博客:15 分钟学会 Immutablejavascript

玄曦.png

1. 什么是 Immutable ?

Immutable Data 就是一旦建立,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操做都会返回一个新的 Immutable 对象。主要原理是采用了 Persistent Data Structure(持久化数据结构),就是当每次修改后咱们都会获得一个新的版本,且旧版本能够无缺保留,也就是使用旧数据建立新数据时,要保证旧数据同时可用且不变。同时为了不 deepCopy 把全部节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),就是对于本次操做没有修改的部分,咱们能够直接把相应的旧的节点拷贝过去,这其实就是结构共享。html

2. Immutable 有什么优势?

2.1 下降复杂度,避免反作用

在 Javascript 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的状况,这样会引起不可控的反作用,以下代码所示:前端

let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
 
console.log(obj1.name); // 李四
复制代码

使用 Immutable 后:java

import { Map } from 'immutable';
let obj1 = Map({ name: '张三'});
let obj2 = obj1;
obj2.set({name:'李四'});
console.log(obj1.get('name'));  // 张三
复制代码

当咱们使用 Immutable 下降了 Javascript 对象 带来的复杂度的问题,使咱们状态变成可预测的。react

2.2 节省内存

Immutable 采用告终构共享机制,因此会尽可能复用内存。git

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 每次修改都会建立一个新对象,且对象不变,那么变动的记录就可以被保存下来,应用的状态变得可控、可追溯,方便撤销和重作功能的实现,请看下面代码示例:github

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,提供了不少易用的数据类型。像CollectionListMapSetRecordSeq,以及一系列操做它们的方法,包括 sort,filter,数据分组,reverse,flatten 以及建立子集等方法,具体 API 请参考官方文档编程

3. 在 react 中如何使用 Immutable

咱们都知道在 React 父组件更新会引发子组件从新 render,当咱们传入组件的 props 和 state 只有一层时,咱们能够直接使用 React.PureComponent,它会自动帮咱们进行浅比较,从而控制 shouldComponentUpdate 的返回值。redux

可是,当传入 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,浅比较就失效了。固然咱们也能够在 shouldComponentUpdate() 中使用使用 deepCopydeepCompare 来避免没必要要的 render() ,但 deepCopydeepCompare 通常都是很是耗性能的。这个时候咱们就须要 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 比较的是两个对象的 hashCodevalueOf(对于 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官方文档

推荐阅读

经过自定义Vue指令实现前端曝光埋点

我在工做中是如何使用 git 的

开源做品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索