mobx專案實踐

尹光耀發表於2018-12-15

由於redux需要寫很多繁瑣的action和reducer,大部分專案也沒有複雜到需要用到redux的程度,導致不少人對redux深惡痛絕。mobx是另一種狀態管理方案,這裡分享一下我最近使用mobx的經驗。

更響應式

我最喜歡mobx的地方就是和vue一樣的資料監聽,底層通過Object.defineProperty或Proxy來劫持資料,對元件可以進行更細粒度的渲染。

在react中反而把更新元件的操作(setState)交給了使用者,由於setState的"非同步"特性導致了沒法立刻拿到更新後的state。

computed

想像一下,在redux中,如果一個值A是由另外幾個值B、C、D計算出來的,在store中該怎麼實現?

如果要實現這麼一個功能,最麻煩的做法是在所有B、C、D變化的地方重新計算得出A,最後存入store。

當然我也可以在元件渲染A的地方根據B、C、D計算出A,但是這樣會把邏輯和元件耦合到一起,如果我需要在其他地方用到A怎麼辦?

我甚至還可以在所有connect的地方計算A,最後傳入元件。但由於redux監聽的是整個store的變化,所以無法準確的監聽到B、C、D變化後才重新計算A。

但是mobx中提供了computed來解決這個問題。正如mobx官方介紹的一樣,computed是基於現有狀態或計算值衍生出的值,如下面todoList的例子,一旦已完成事項數量改變,那麼completedCount會自動更新。

class TodoStore {
    @observable todos = []
    @computed get completedCount() {
		return (this.todos.filter(todo => todo.isCompleted) || []).length
	}
}
複製程式碼

reaction

reaction則是和autorun功能類似,但是autorun會立即執行一次,而reaction不會,使用reaction可以在監聽到指定資料變化的時候執行一些操作,有利於和副作用程式碼解耦。

// 當todos改變的時候將其存入快取
reaction(
    () => toJS(this.todos),
    (todos) =>  localStorage.setItem('mobx-react-todomvc-todos', JSON.stringify({ todos }))
)
複製程式碼

依賴收集

在mobx中,通過autorun和reaction對依賴的資料進行了收集(可以通過get來收集),一旦這些資料發生了變化,就會執行接受到的函式,和釋出訂閱很相似。

mobx-react中則提供了observer方法,用來收集元件依賴的資料,一旦這些資料變化,就會觸發元件的重新渲染。

管理區域性狀態

在react中,我們更新狀態需要使用setState,但是setState後並不能立馬拿到更新後的state,雖然setState提供了一個回撥函式,我們也可以用Promise來包一層,但終究還是個非同步的方式。

在mobx中,我們可以直接在react的class裡面用observable宣告屬性來代替state,這樣可以立馬拿到更新後的值,而且observer會做一些優化,避免了頻繁render。

@observer
class App extends React.Component {
  @observable count = 0;
  constructor(props) {
    super(props);
  }
  @action
  componentDidMount() {
    this.count = 1;
    this.count = 2;
    this.count = 3;
  }
  render() {
    return <h1>{this.count}</h1>
  }
}
複製程式碼

拆分store

mobx中的store的建立偏向於物件導向的形式,mobx官方給出的例子todomvc中的store更接近於mvc中的model。

但是這樣也會帶來一個問題,業務邏輯我們應該放到哪裡?如果也放到store裡面很容易造成不同store之間資料的耦合,因為業務程式碼必然會耦合不同的資料。

我參考了dobjs後,推薦將store拆分為action和dataModel兩種。

action和dataModel一起組合成了頁面的總store,dataModel只存放UI資料以及只涉及自身資料變化的action操作(在mobx嚴格模式中,修改資料一定要用action或flow)。

action store則是負責存放一些需要使用來自不同store資料的action操作。 我個人理解,dataModel更像MVC中的model,action store是controller,react components則是view,三者構成了mvc的結構。

- stores
    - actions
        - hotelListAction.js
    - dataModel
        - globalStatus.js
        - hotelList.js
    - index.js
// globalStatus
class GlobalStatus {
    @observable isShowLoading = false;
    @action showLoading = () => {
        this.isShowLoading = true
    }
    @action hideLoading = () => {
        this.isShowLoading = false
    }
}
// hotelList
class HotelList {
    @observable hotels = []
    @action addHotels = (hotels) => {
        this.hotels = [...toJS(this.hotels), ...hotels];
    }
}
// hotelListAction
class HotelListAction {
    fetchHotelList = flow(function *() {
        const {
            globalStatus,
            hotelList
        } = this.rootStore
        globalStatus.showLoading();
        try {
            const res = yield fetch('/hoteList', params);
            hotelList.addHotels(res.hotels);
        } catch (err) {
        } finally {
            globalStatus.hideLoading();
        }
    }).bind(this)
}
複製程式碼

store結構

細粒度的渲染是高效的

observer可以給元件增加訂閱功能,一旦收到資料變化的通知就會將元件重新渲染,從而做到更細粒度的更新,這是redux和react很難做到的,因為react中元件重新渲染基本是依賴於setState和接收到新的props,子元件的渲染幾乎一定會伴隨著父元件的渲染。

也許很多人沒有注意到,mobx-react中還提供了一個Observer元件,這個元件接收一個render方法或者render props。

const App = () => <h1>hello, world</h1>;
<Observer>{() => <App />}</Observer>
<Observer render={() => <App />} />
複製程式碼

也許你要問這個和observer有什麼區別?還寫的更加複雜了,下面這個例子對比起來會比較明顯。

import { observer, Observer, observable } from 'mobx-react'
const App = observer(
    (props) => <h1>hello, {props.name}</h1>
)
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = observer(
    (props) => {
        return (
            <>
                <Header />
                <App name={props.person.name} />
                <Footer />
            </>
        )
    }
)
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
複製程式碼

上面這個程式碼,Container元件監聽到person.name改變的時候會重新渲染,這樣就導致了原本不需要重新渲染的Header和Footer也跟著渲染了,如果使用Observer就可以做到更細粒度的渲染。

const App = (props) => <h1>hello, {props.name}</h1>
const Header = (props) => <h1>this is header</h1>
const Footer = (props) => <h1>this is footer</h1>
const Container = (props) => {
    return (
        <>
            <Header />
            <Observer render={
                () => <App name={props.person.name} />
            }>
            <Footer />
        </>
    )
}
const person = observable({name: "gyyin"});
render(<Container person={person} />, document.getElementById("app"));
person.name = "world";
複製程式碼

如果在Header和Footer裡面做console.log,你會發現只有被Observer包裹的App元件進行了重新渲染,由於Container沒有訂閱資料變化,所以也不會重新渲染。

但如果不是對效能有極致的追求,observer已經足夠了,大量的Observer會花費你很多精力來管理渲染問題。

本文如有錯誤之處,希望大家能夠指出,一起討論。

參考連結:

  1. 如何組織Mobx中的Store之一:構建State、拆分Action
  2. 面向未來的前端資料流框架 - dob
  3. 為什麼我們需要reselect

PS:歡迎大家關注我的公眾號【前端小館】,大家一起來討論技術。

mobx專案實踐

相關文章