mobx項目實踐

因爲redux須要寫不少繁瑣的action和reducer,大部分項目也沒有複雜到須要用到redux的程度,致使很多人對redux深惡痛絕。mobx是另外一種狀態管理方案,這裏分享一下我最近使用mobx的經驗。javascript

更響應式

我最喜歡mobx的地方就是和vue同樣的數據監聽,底層經過Object.defineProperty或Proxy來劫持數據,對組件能夠進行更細粒度的渲染。前端

在react中反而把更新組件的操做(setState)交給了使用者,因爲setState的"異步"特性致使了無法馬上拿到更新後的state。vue

computed

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

若是要實現這麼一個功能,最麻煩的作法是在全部B、C、D變化的地方從新計算得出A,最後存入store。react

固然我也能夠在組件渲染A的地方根據B、C、D計算出A,可是這樣會把邏輯和組件耦合到一塊兒,若是我須要在其餘地方用到A怎麼辦?redux

我甚至還能夠在全部connect的地方計算A,最後傳入組件。但因爲redux監聽的是整個store的變化,因此沒法準確的監聽到B、C、D變化後才從新計算A。segmentfault

可是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能夠在監聽到指定數據變化的時候執行一些操做,有利於和反作用代碼解耦。mvc

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

依賴收集

在mobx中,經過autorun和reaction對依賴的數據進行了收集(能夠經過get來收集),一旦這些數據發生了變化,就會執行接受到的函數,和發佈訂閱很類似。app

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:歡迎你們關注個人公衆號【前端小館】,你們一塊兒來討論技術。

相關文章
相關標籤/搜索