因爲redux須要寫不少繁瑣的action和reducer,大部分項目也沒有複雜到須要用到redux的程度,致使很多人對redux深惡痛絕。mobx是另外一種狀態管理方案,這裏分享一下我最近使用mobx的經驗。javascript
我最喜歡mobx的地方就是和vue同樣的數據監聽,底層經過Object.defineProperty或Proxy來劫持數據,對組件能夠進行更細粒度的渲染。前端
在react中反而把更新組件的操做(setState)交給了使用者,因爲setState的"異步"特性致使了無法馬上拿到更新後的state。vue
想像一下,在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則是和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>
}
}
複製代碼
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:歡迎你們關注個人公衆號【前端小館】,你們一塊兒來討論技術。