由於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)
}
複製程式碼
細粒度的渲染是高效的
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會花費你很多精力來管理渲染問題。
本文如有錯誤之處,希望大家能夠指出,一起討論。
參考連結:
PS:歡迎大家關注我的公眾號【前端小館】,大家一起來討論技術。