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

相關文章
相關標籤/搜索