React-redux進階之Immutable.js

Immutable.js

Immutable的優勢

1. 保證不可變(每次通過Immutable.js操作的對象都會返回一個新的對象)  
2. 豐富的API  
3. 性能好 (通過字典樹對數據結構的共享)

<br/>
Immutable的問題

1. 與原生JS交互不友好 (通過Immutable生成的對象在操作上與原生JS不同,如訪問屬性,myObj.prop1.prop2.prop3 => myImmutableMap.getIn([‘prop1’, ‘prop2’, ‘prop3’])。另外其他的第三方庫可能需要的是一個普通的對象)  
2. Immutable的依賴性極強 (一旦在代碼中引入使用,很容易傳播整個代碼庫,並且很難在將來的版本中移除)  
3. 不能使用解構和對象運算符 (相對來說,代碼的可讀性差)  
4. 不適合經常修改的簡單對象 (Immutable的性能比原生慢,如果對象簡單,並且經常修改,不適合用)  
5. 難以調試 (可以採用 Immutable.js Object Formatter擴展程序協助)  
6. 破壞JS原生對象的引用,造成性能低下 (toJs每次都會返回一個新對象)

<br/>

原生Js遇到的問題

原生Js遇到的問題

// 場景一
var obj = {a:1, b:{c:2}};
func(obj);
console.log(obj)  //輸出什麼??

// 場景二
var obj = ={a:1};
var obj2 = obj;
obj2.a = 2;
console.log(obj.a);  // 2
console.log(obj2.a);  // 2

代碼來源:https://juejin.im/post/5948985ea0bb9f006bed7472
// ajax1
this.props.a = {
  data: 1,
}
// ajax2
nextProps.a = {
  data: 1,
}
//shouldComponentUpdate()
shallowEqual(this.props, nextProps) // false
// 數據相同但是因爲引用不同而造成不必要的re-rederning
由於Js中的對象是引用類型的,所以很多時候我們並不知道我們的對象在哪裏被操作了什麼,而在Redux中,因爲Reducer是一個純函數,每次返回的都是一個新的對象(重新生成對象佔用時間及內存),再加上我們使用了connect這個高階組件,官方文檔中雖然說react-redux做了一些性能優化,但終究起來,react-redux只是對傳入的參數進行了一個淺比較來進行re-redering(爲什麼不能在mapStateToProps中使用toJs的原因)。再進一步,假如我們的state中的屬性嵌套了好幾層(隨着業務的發展),對於原來想要的數據追蹤等都變得極爲困難,更爲重要的是,在這種情況下,我們一些沒有必要的組件很可能重複渲染了多次。  

<br/>

總結起來就是以下幾點(問題雖少,但都是比較嚴重的):

1. 無法追蹤Js對象  
2. 項目複雜時,reducer生成新對象性能低  
3. 只做淺比較,有可能會造成re-redering不符合預期(多次渲染或不更新)

<br/>

爲什麼不使用深比較

或許有人會疑惑,爲什麼不使用深比較來解決re-redering的問題,答案很簡單,因爲消耗非常巨大~  
想象一下,如果你的參數複雜且巨大, 對每一個進行比較是多麼消耗時間的一件事~  

<br/>

使用Immutable解決問題

項目複雜後, 追蹤困難

使用Immutable之後,這個問題自然而然就解決了。所謂的追蹤困難,無非就是因爲對象是mutable的,我們無法確定它到底何時何處被改變,而Immutable每次都會保留原來的對象,重新生成一個對象,(與redux的純函數概念一樣)。但也要注意寫代碼時的習慣:

// javascript
const obj = { a: 1 }
function (obj) {
  obj.b = 2
  ...
}

// Immutable
const obj = Map({ a : 1 })
function (obj) {
  const obj2 = obj.set({ 'b', 2 })
}

<br/>

reducer生成新對象性能差
當項目變得複雜時,每一次action對於生成的新state都會消耗一定的性能,而Immutable.js在這方面的優化就很好。或許你會疑惑爲什麼生成對象還能優化?請往下看~

在前面就講到,Immutable是通過字典樹來做==結構共享==的

image
(圖片來自網絡)

這張圖的意思就是

immutable使用先進的tries(字典樹)技術實現結構共享來解決性能問題,當我們對一個Immutable對象進行操作的時候,ImmutableJS會只clone該節點以及它的祖先節點,其他保持不變,這樣可以共享相同的部分,大大提高性能。

<br/>

re-rendering不符合預期

其實解決這個問題是我們用Immutable的主要目的,先從淺比較說起
淺比較引起的問題在這之前已經講過,事實上,即使Immutable之後,connect所做的依然是淺比較,但因爲Immutable每次生成的對象引用都不同,哪怕是修改的是很深層的東西,最後比較的結果也是不同的,所以在這裏解決了第一個問題,==re-rendering可能不會出現==。
但是, 我們還有第二個問題, ==沒必要的re-rendering==,想要解決這個問題,則需要我們再封裝一個高階組件,在這之前需要了解下Immutable的 is API

// is()   判斷兩個immutable對象是否相等
immutable.is(imA, imB);

這個API有什麼不同, ==這個API比較的是值,而不是引用==,So: 只要兩個值是一樣的,那麼結果就是true

const a = Immutable.fromJS({
  a: {
    data: 1,
  },
  b: {
    newData: {
      data: 1
    }
  }
})
const target1 = a.get('a')
const target2 = a.getIn(['b', 'newData'])
console.log(Immutable.is(target1, target2)) //is比較的依據就是每個值的hashcode
// 這個hashcode就相當於每個值的一個ID,不同的值肯定有不同的ID,相同的ID對應着的就是相同的值。

也就是說,對於下面的這種情況, 我們可以不用渲染

// ajax1
this.props.a = {
  data: 1,
}
// ajax2
nextProps.a = {
  data: 1,
}
//shouldComponentUpdate()
Immutable.is(this.props, nextProps) // true

最後, 我們需要封裝一個高階組件來幫助我們統一處理是否需要re-rendering的情況

//baseComponent.js   component的基類方法

import React from 'react';
import {is} from 'immutable';

class BaseComponent extends React.Component {
    constructor(props, context, updater) {
        super(props, context, updater);
    }

    shouldComponentUpdate(nextProps, nextState) {
        const thisProps = this.props || {};
        const thisState = this.state || {};
        nextState = nextState || {};
        nextProps = nextProps || {};

        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 (!is(thisProps[key], nextProps[key])) {
                return true;
            }
        }

        for (const key in nextState) {
            if (!is(thisState[key], nextState[key])) {
                return true;
            }
        }
        return false;
    }
}

export default BaseComponent;

代碼來源鏈接:https://juejin.im/post/5948985ea0bb9f006bed7472

<br/>

使用Immutable需要注意的點

使用Immutable需要注意的點

1. 不要混合普通的JS對象和Immutable對象 (不要把Imuutable對象作爲Js對象的屬性,或者反過來)  
2. 對整顆Reudx的state樹作爲Immutable對象  
3. 除了展示組件以外,其他地方都應該使用Immutable對象 (提高效率,而展示組件是純組件,不應該使用) 
4. 少用toJS方法 (一個是因爲否定了Immutable,另外則是操作非常昂貴)  
5. 你的Selector應該永遠返回Immutable對象 (即mapStateToProps,因爲react-redux中是通過淺比較來決定是否re-redering,而使用toJs的話,每次都會返回一個新對象,即引用不同)

<br/>

通過高階組件,將Immutable對象轉爲普通對象傳給展示組件

1. 高階組件返回一個新的組件,該組件接受Immutable參數,並在內部轉爲普通的JS對象  
2. 轉爲普通對象後, 新組件返回一個入參爲普通對象的展示組件
import React from 'react'
import { Iterable } from 'immutable'

export const toJS = WrappedComponent => wrappedComponentProps => {
  const KEY = 0
  const VALUE = 1

  const propsJS = Object.entries(wrappedComponentProps).reduce(
    (newProps, wrappedComponentProp) => {
      newProps[wrappedComponentProp[KEY]] = Iterable.isIterable(
        wrappedComponentProp[VALUE]
      )
        ? wrappedComponentProp[VALUE].toJS()
        : wrappedComponentProp[VALUE]
      return newProps
    },
    {}
  )

  return <WrappedComponent {...propsJS} />
}
import { connect } from 'react-redux'

import { toJS } from './to-js'
import DumbComponent from './dumb.component'

const mapStateToProps = state => {
  return {
    // obj is an Immutable object in Smart Component, but it’s converted to a plain
    // JavaScript object by toJS, and so passed to DumbComponent as a pure JavaScript
    // object. Because it’s still an Immutable.JS object here in mapStateToProps, though,
    // there is no issue with errant re-renderings.
    obj: getImmutableObjectFromStateTree(state)
  }
}
export default connect(mapStateToProps)(toJS(DumbComponent))

參考

<html>
Immutable.js 以及在 react+redux 項目中的實踐<br/>
Using Immutable.JS with Redux<br/>
不變應萬變-Immutable優化React<br/>
React-Redux分析<br/>
</html>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章