Redux vs Mobx系列(-):immutable vs mutable

ykforerlang發表於2018-03-15

**注意:**我會寫多篇文章來比較說明redux和mobx的不同,redux和mobx各有優缺點, 如果對React/Mobx/Redux都理解夠深刻,我個人推薦Mobx(逃跑。。。)

React社群的大方向是immutable, 不管是用immutable.js 還是函數語言程式設計使用不可變資料結構。為什麼React需要不可變資料結構呢? 考慮下面的一個應用

應用結構

class Root extends Component {
    state = {
        something: 'sh'
    }
    render() {
        return (
            <div>
                <div onClick={e => { // onClick  setState 空物件
                    this.setState({})
                }}>click me!!</div>
                <L1/>
                <Dog sh={this.state.something}/>
            </div>
        )
    }
}

...
class L1 extends Component {
    render() {
        console.log('invoke L1')
        return (
            <div>
                <L11/>
                <L12/>
            </div>
        )
    }
}
...
class L122 extends Component {
    render() {
        console.log('invoke L122')
        return (
            <div>L122</div>
        )
    }
}
複製程式碼

當我點選 Root上的 click me 的時候, 執行了this.setState({}),於是觸發Root更新, 這個時候L1, Dog會怎麼樣呢? 結論是當點選的時候 控制檯會列印:

invoke L1
invoke L11
invoke L111
invoke L112
invoke L12
invoke L121
invoke L122
invoke Dog
複製程式碼

當一個元件需要跟新的時候,react並不知道哪裡會更新,在內部react會用object(存js物件)來代表dom結構, 當有更新的時候 react暴力比較前後object的差異,增量的處理更新的dom部分。 對於剛才的這個例子, react暴力計算的結果就是沒有增量。。。雖然react暴力比較演算法已經非常高效了,這些無意義的計算也應該避免, 起碼可以節省計算機的電 --> 少用煤 --> 減少二氧化碳排放 --> 保護地球。 畢竟 蝴蝶效應!

ui = f(d) 相同的d得到相同的ui(設計元件的時候最好這樣)。例如我們上例的Dog,我們可以直接比較sh

class Dog extends Component {
    shouldComponentUpdate(nextProps) {
        return this.props.sh !== nextProps.sh
    }
    ...
}
複製程式碼

更加一般的情況, 我們怎麼確定元件的props和state沒有變化呢? 不可變物件 ! 如果物件是不可變的, 那麼當物件a !== a' 就代表這是2個物件,不相等。而在傳統可變的物件中 需要deepEqual(a, a')。 如果我們的React應用裡面 props和state都是不可變物件, 那麼:

class X extends Component {
     shouldComponentUpdate(nextProps, nextState) {
       return !( shallowEqual(this.props, nextProps) && shallowEqual(this.state, nextState))
    }
}
複製程式碼

react也考慮到了一點 提供了PureComponent幫助我們預設做了這個shouldComponentUpdate

把 L1, Dog, L11 ... L122改為PureComponent, 再次點選,列印:

  // 沒有輸出。。。
複製程式碼

拯救了地球!

Redux

redux 每次action發生的時候,都會返回一個全新的state,�天生是immutable。 Redux + PureComponent 輕鬆開發出高效web應用

Mobx

Mobx剛好相反,它依賴副作用(so 所有元件不在繼承PureComponent), 那它是怎麼工作的呢?

mobx-react的 @observer通過收集元件 render函式依賴的狀態, 當狀態有修改的時候精確的控制元件的更新。

比如現在 Root元件依賴狀態 title, L122 依賴狀態x(Root傳遞x給L1,L1傳遞給L12, L12傳遞給L122)。 那麼應該:

const store = observable({
    x: 'x'
    title: 'title',
})

window.store = store
@observer
export default class MobxRoot extends Component {
    render() {
        console.log('invoke MobxRoot')
        const { title, x } = store
        return (
            <div>
                <div>{title}</div>
                <L1 x={x}/>
                <Dog/>
            </div>
        )
    }
}
class L1 extends Component {
    render() {
        console.log('invoke L1')
        return (
            <div>
                <L11/>
                <L12 x={this.props.x}/>
            </div>
        )
    }
}
class L12 extends Component {
     render() {
        console.log('invoke L12')
        return (
            <div>
                <L121/>
                <L122 x={this.props.x}/>
            </div>
        )
    }
}
@observer
class L122 extends Component {
     render() {
        console.log('invoke L122')
        return (
            <div>
                { this.props.x || 'L122'}
            </div>
        )
    }
}
複製程式碼

這樣當title變化的時候, Mobx發現只有MobxRoot元件關心title,於是更新MobxRoot, 當x變化的時候 Mobx發現有MobxRoot, L122 依賴與x,於是更新MobxRoot,L122 。 工作很正常。

細想當title變化的時候,更新MobxRoot,由於更新了MobxRoot進而導致L1,Dog的遞迴暴力diff計算,顯而易見的是無意義的計算。 當x變化的時候呢, 由於MobxRoot,L122依賴了x, 會先更新MobxRoot,然後更新L122,然而在更新MobxRoot的時候又會遞迴的更新到L122, 這裡更加麻煩了(實際上React不會更新兩次L122)。

Mobx也在文件裡指出了這個問題(晚一點使用間接引用值), 對應的解決方法是 L1 先傳遞store。。。最後在L122裡面從store裡面獲取x。

這裡暴露了兩個問題:

  1. 父元件的更新,會影響到子元件,由於不是使用不可變資料,還不能簡單的通過PureComponent優化
  2. props傳遞的過程中 不可避免的會提前使用引用值,導致某些元件無意義的更新, 狀態越多越複雜

記住在mobx應用裡, 應該把元件是否更新的絕對權完全交給Mobx,完全交給Mobx,完全交給Mobx。 即使是父元件也不應該引起子元件的跟新。 所以所有的元件(沒有被@observer修飾)都應該繼承與PureComponent(這裡的PureComponent的作用已經不是原來的了, 這裡的作用是阻止更新行為的傳遞)。 另外一點, 由於元件是否更新取決與Mobx, 元件更新的資料又取值與Mobx,所以還有必要props傳遞嗎? 基於這兩點程式碼:

const store = observable({
    x: 'x'
    title: 'title',
})

window.store = store
@observer
export default class MobxRoot extends Component {
    render() {
        console.log('invoke MobxRoot')
        const { title} = store
        return (
            <div>
                <div>{title}</div>
                <L1/>
                <Dog/>
            </div>
        )
    }
}
class L1 extends PureComponent {
    render() {
        console.log('invoke L1')
        return (
            <div>
                <L11/>
                <L12/>
            </div>
        )
    }
}
class L12 extends PureComponent {
     render() {
        console.log('invoke L12')
        return (
            <div>
                <L121/>
                <L122/>
            </div>
        )
    }
}
@observer
class L122 extends Component {
     render() {
        console.log('invoke L122')
        const x = window.store // 直接從Mobx獲取
        return (
            <div>
                { x || 'L122'}
            </div>
        )
    }
}
複製程式碼

這樣當title改變的時候, 只有MobxRoot會跟新, 當x改變的時候只有L122 會更新。 現在我們可以把應用裡面的所有元件分為兩類: 關注狀態的@observer元件, 其他PureComponent元件。這樣每當有狀態改變的時候, Mobx精確控制需要更新的@observer元件(最小的更新集合),其他PureComponent阻止無意義的更新。 問題的關鍵是開發者一定要搞清楚 哪些元件需要 @observer。 這個問題先放一下, 我們在看一個mobx的問題

假設L122複用了一個第三方庫提供的元件(表明我們不能修改這個元件)

@observer
class L122 extends Component {
     render() {
        console.log('invoke L122')
        const x = window.store // 直接從Mobx獲取
        return (
            <div>
                <BigComponent x={x}/>
            </div>
        )
    }
}
複製程式碼

元件 BigComponent 正如其名 是一個很‘大’的元件,他接收一個props物件 x,x結構如下:

x = {
   name: 'n'
   addr: '',
}
複製程式碼

此時當我們執行: window.store.x.name = 'fcdcd' 的時候, 我們期待的是BigComponent按照我們的意願,根據改變後的x重新渲染, 其實不會。 因為在這裡沒有任何元件 依賴name, 為了讓L122 正常工作, 我們必須:

@observer
class L122 extends Component {
     render() {
        console.log('invoke L122')
        const x = window.store.x 
        const nx = {
            name: x.name,
            addr: x.addr
        }
        
        return (
            <div>
                <BigComponent x={nx}/>
            </div>
        )
    }
}
複製程式碼

如果不明白mobx的原理, 可能會很疑惑,疑惑這裡為什麼要這麼寫, 疑惑哪裡為啥不更新, 疑惑哪裡為啥莫名其妙更新了。。。

什麼元件需要@observer? 當一個render方法裡,出現我們不能控制的元件(包括原生標籤, 第三方庫元件)依賴於狀態的時候, 我們應該使用@observer, 其他元件應該繼承PureComponent。 這樣我們的應用在狀態傳送改變的時候,更新的集合最小,效能最高。

除此之外,Mobx還有一個效能隱患,希望mobx的擁護者能夠清楚的認知到,假設現在 L122 不僅也依賴title, 還依賴狀態a, b, c, d, e, f, g, h:

class L122 extends Component {
     render() {
         console.log('invoke L122')
       const { title, a, b, c, d, e, f, g, h } = window.store
        
        return (
            <div>
               <span>{title}</span>
               <span>{a}</span>
               <span>{b}</span>
               ...
               
               <span>{h}</span>
            </div>
        )
    }
}

function changeValue() {
    window.store.title = 't'
    window.store.a = 'a1'
    window.store.b = 'b1'
    window.store.c = 'c1'
}
複製程式碼

當執行 changeValue()的時候 會發生什麼呢?控制檯會列印:

invoke MobxRoot
invoke L122
invoke L122
invoke L122
invoke L122
複製程式碼

一身冷汗!!得好好想想這裡的資料層設計, 是否把這幾個屬性組成一個物件,狀態越來越複雜的時候可能不是那麼簡單。

第三方庫結合

redux與第三方庫結合沒有好說的,工作的很好。 很多庫現在已經假定了 傳人的狀態是 不可變的。

mobx正如前文所說 不管是釋出為第三方庫, 還是使用第三方庫

  1. mobx寫的元件,釋出給其他應用使用比較困難,因為要不我們直接從全域性取資料渲染(context獲取 道理相同), 要不推遲引用值的獲取, 不管是哪一種,元件都沒有任何可讀性。
  2. mobx 使用第三方 例如BigComponent, 沒有那麼自然。

開發效率

這裡我們只說 immutable的開發效率,mutable的開發效率應該是最低的。 0. 結合物件展開浮, js裸寫。 也不難

  1. immutable.js 學習成本略高, 包大小也畢竟大
  2. 函數語言程式設計,專案組自己一個人 可以考慮
  3. immer 如果不考慮IE,強烈推薦, 強烈推薦 (作者是mobx的作者)。 immer和mutable的修改資料的方法是一摸一樣的, 最後會根據你的修改返回一個不可變的物件。 github地址

結論

如果你能無痛的處理immutable, 那麼Redux + PureComponent 很方便寫出高效能的應用。

如果你對Mobx掌握的足夠好, 那麼Mobx絕對會迅速的提高開發效率。

本文程式碼github地址


相關文章