我為什麼從Redux遷移到了Mobx

有贊前端發表於2017-11-29

Redux是一個資料管理層,被廣泛用於管理複雜應用的資料。但是實際使用中,Redux的表現差強人意,可以說是不好用。而同時,社群也出現了一些資料管理的方案,Mobx就是其中之一。

Redux的問題

Predictable state container for JavaScript apps

這是Redux給自己的定位,但是這其中存在很多問題。
首先,Redux做了什麼?看Redux的原始碼,createStore只有一個函式,返回4個閉包。dispatch只做了一件事,呼叫reducer然後呼叫subscribelistener,這其中state的不可變或者是可變全部由使用者來控制,Redux並不知道state有沒有發生變化,更不知道state具體哪裡發生了變化。所以,如果view層需要知道哪一部分需要更新,只能通過髒檢查。

再看react-redux做了什麼,在store.subscribe上掛回撥,每次發生subscribe就呼叫connect傳進去mapStateToPropsmapDispatchToProps,然後髒檢測props的每一項。當然,我們可以利用不可變資料的特點,去減少prop的數量從而減少髒檢測的次數,但是哪有props都來自同一個子樹這麼好的事呢?

所以,如果有n個元件connect,每當dispatch一個action的時候,無論做了什麼粒度的更新,都會發生O(n)時間複雜度的髒檢測。

// Redux 3.7.2 createStore.js

// ...
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
// ...複製程式碼

更糟糕的是,每次reducer執行完Redux就直接呼叫listener了,如果在短時間內發生了多次修改(例如使用者輸入),不可變的開銷,加上redux用字串匹配action的開銷,髒檢測的開銷,再加上view層的開銷,整個效能表現會非常糟糕,即使在使用者輸入的時候往往只需要更新一個"input"。應用規模越大,效能表現越糟糕。(這裡的應用指單個頁面。這裡的單頁不是SPA的單頁的意思,因為有Router的情況下,被切走的頁面其所有元件都沒unmount了)

在應用規模增大的同時,非同步請求數量一多,Redux所宣傳的Predictable也根本就是泡影,更多的時候是配合各種工具淪為資料視覺化工具。

Mobx

Mobx可以說是眾多資料方案中最完善的一個了。Mobx本身獨立,不與任何view層框架互相依賴,因此你可以隨意選擇合適的view層框架(部分除外,例如Vue,因為它們的原理是一樣的)。

目前Mobx(3.x)和Vue(2.x)採用了相同的響應式原理,借用Vue文件的一張圖:
我為什麼從Redux遷移到了Mobx
為每個元件建立一個Watcher,在資料的getter和setter上加鉤子,當元件渲染的時候(例如,呼叫render方法)會觸發getter,然後把這個元件對應的Watcher新增到getter相關的資料的依賴中(例如,一個Set)。當setter被觸發時,就能知道資料發生了變化,然後同時對應的Watcher去重繪元件。

這樣,每個元件所需要的資料時精確可知的,因此當資料發生變化時,可以精確地知道哪些元件需要被重繪,資料變化時重繪的過程是O(1)的時間複雜度。

需要注意的是,在Mobx中,需要把資料宣告為observable。

import React from 'react';
import ReactDOM from 'react-dom';
import { observable, action } from 'mobx';
import { Provider, observer, inject } from 'mobx-react';

class CounterModel {
    @observable
    count = 0

    @action
    increase = () => {
        this.count += 1;
    }
}

const counter = new CounterModel();

@inject('counter') @observer
class App extends React.Component {
    render() {
        const { count, increase } = this.props.counter;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(
    <Provider counter={counter}>
        <App />
    </Provider>
);複製程式碼

效能

在這篇文章中,作者使用了一個一個128*128的繪圖板來說明問題。
由於Mobx利用gettersetter(未來可能會出現一個平行的基於Proxy的版本)去收集元件例項的資料依賴關係,因此每單當一個點發生更新的時候,Mobx知道哪些元件需要被更新,決定哪個元件更新的過程的時間複雜度是O(1)的,而Redux通過髒檢查每一個connect的元件去得到哪些元件需要更新,有n個元件connect這個過程的時間複雜度就是O(n),最終反映到Perf工具上就是JavaScript的執行耗時。

雖然在經過一系列優化後,Redux的版本可以獲得不輸Mobx版本的效能,當時Mobx不用任何優化就可以得到不錯的效能。而Redux最完美的優化是為每一個點建立單獨的store,這與Mobx等一眾精確定位資料依賴的方案在思想上是相同的。

Mobx State Tree

Mobx並不完美。Mobx不要求資料在一顆樹上,因此對Mobx進行資料可是化或者是記錄每次的資料變化變得不太容易。在Mobx的基礎上,Mobx State Tree誕生了。同Redux一樣,Mobx State Tree要求資料在一顆樹上,這樣對資料進行視覺化和追蹤就變得非常容易,對開發來說是福音。同時Mobx State Tree非常容易得到準確的TypeScript型別定義,這一點Redux不容易做到。同時還提供了執行時的型別安全檢查。

import React from 'react';
import ReactDOM from 'react-dom';
import { types } from 'mobx-state-tree';
import { Provider, observer, inject } from 'mobx-react';

const CountModel = types.model('CountModel', {
    count: types.number
}).actions(self => ({
    increase() {
        self.count += 1;
    }
}));

const store = CountModel.create({
    count: 0
});

@inject(({ store }) => ({ count: store.count, increase: store.increase }))
class App extends React.Component {
    render() {
        const { count, increase } = this.props;

        return (
            <div>
                <span>{count}</span>
                <button onClick={increase}>increase</button>
            </div>
        )
    }
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>
);複製程式碼

Mobx State Tree還提供了snapshot的功能,因此雖然MST本身的資料可變,依然能打到不可變的資料的效果。官方提供了利用snaptshot直接結合Redux的開發工具使用,方便開發;同時官方還提供了把MST的資料作為一個Redux的store來使用;當然,利用snapshot也可以MST嵌在Redux的store中作為資料(類似在Redux中很流行的Immutable.js的作用)。

// 連線Redux的開發工具
// ...
connectReduxDevtools(require("remotedev"), store);
// ...

// 直接作為一個Redux store使用
// ...
import { Provider, connect } from 'react-redux';

const store = asReduxStore(store);

@connect(// ...)
function SomeComponent() {
    return <span>Some Component</span>
}

ReactDOM.render(
    <Provider store={store}>
        <App />
    <Provider />,
    document.getElementById('foo')
);

// ...複製程式碼

並且,在MST中,可變資料和不可變的資料(snapshot)可以互相轉化,你可以隨時把snapshot應用到資料上。

applySnapshot(counter, {
    count: 12345
});複製程式碼

除此之外,官方還提供了非同步action的支援。由於JavaScript的限制,非同步操作難以被追蹤,即時使用了async函式,其執行過程中也是不能被追蹤的,就會出現雖然在async的函式內操作了資料,這個async函式也被標記為action,但是會被誤判是在action外修改了資料。以往非同步action只能通過多個action組合使用來完成,而Vue則是通過把action和mutation分開來實現。在Mobx State Tree利用了Generator,使非同步操作可以在一個action函式內完成並且可以被追蹤。

// ...

SomeModel.actions(self => ({
    someAsyncAction: process(function* () {
        const a = 1;
        const b = yield foo(a); // foo必須返回一個Promise
        self.bar = b;
    })
}));

// ...複製程式碼

總結

Mobx利用gettersetter來收集元件的資料依賴關係,從而在資料發生變化的時候精確知道哪些元件需要重繪,在介面的規模變大的時候,往往會有很多細粒度更新,雖然響應式設計會有額外的開銷,在介面規模大的時候,這種開銷是遠比對每一個元件做髒檢查小的,因此在這種情況下Mobx會很容易得到比Redux更好的效能。而在資料全部發生改變時,基於髒檢查的實現會比Mobx這類響應式有更好的效能,但這類情況很少。同時,有些benchmark並不是最佳實踐,其結果也不能反映真實的情況。

但是,由於React本身提供了利用不可變資料結構來減少無用渲染的機制(例如PureComponent,函式式元件),同時,React的一些生態和Immutable繫結了(例如Draft.js),因此在配合可變的觀察者模式的資料結構時並不是那麼舒服。所以,在遇到效能問題之前,建議還是使用Redux和Immutable.js搭配React。

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.

一些實踐

由於JavaScript的限制,一些物件不是原生的物件,其他的型別檢查庫可能會導致意想不到的結果。例如在Mobx中,陣列並不是一個Array,而是一個類Array的物件,這是為了能監聽到資料下標的賦值。相對的,在Vue中陣列是一個Array,但是陣列下標賦值要使用splice來進行,否則無法被檢測到。

由於Mobx的原理,要做到精確的按需更新,就要在正確的地方觸發getter,最簡單的辦法就是render要用到的資料只在render裡解構。mobx-react從4.0開始,inject接受的map函式中的結構也會被追蹤,因此可以直接用類似react-redux的寫法。注意,在4.0之前inject的map函式不會被追蹤。

響應式有額外的開銷,這些開銷在渲染大量資料時會對效能有影響(例如:長列表),因此要合理搭配使用observable.refobservable.shallow(Mobx),types.frozen(Mobx State Tree)。

本文首發於有贊技術部落格

相關文章