Mobx是一個功能強大,上手非常容易的狀態管理工具。就連redux的作者也曾經向大家推薦過它,在不少情況下你的確可以使用Mobx來替代掉redux。
本教程旨在介紹其用法及概念,並重點介紹其與React的搭配使用。
先來看看最基本的用法。
observable和autorun
import { observable, autorun } from 'mobx'; const value = observable(0); const number = observable(100); autorun(() => { console.log(value.get()); }); value.set(1); value.set(2); number.set(101);
可以看到,控制台中依次輸出0,1,2。
observable可以用來觀測一個數據,這個數據可以數字、字符串、數組、對象等類型(相關知識點具體會在后文中詳述),而當觀測到的數據發生變化的時候,如果變化的值處在autorun中,那么autorun就會自動執行。
上例中的autorun函數中,只對value值進行了操作,而並沒有number值的什么事兒,所以number.set(101)
這步並不會觸發autorun,只有value的變化才觸發了autorun。
計算屬性——computed
假如現在我們一個數字,但我們對它的值不感興趣,而只關心這個數組是否為正數。這個時候我們就可以用到computed這個屬性了。
const number = observable(10); const plus = computed(() => number.get() > 0); autorun(() => { console.log(plus.get()); }); number.set(-19); number.set(-1); number.set(1);
依次輸出了true,false,true。
第一個true是number初始化值的時候,10>0為true沒有問題。
第二個false將number改變為-19,輸出false,也沒有問題。
但是當-19改變為-1的時候,雖然number變了,但是number的改變實際上並沒有改變plus的值,所以沒有其它地方收到通知,因此也就並沒有輸出任何值。
直到number重新變為1時才輸出true。
實際項目中,computed會被廣泛使用到。
const price = observable(199); const number = observable(15); //computed的其它簡單例子 const allPrice = computed(() => price.get() * number.get());
順便一提,computed屬性和React Native中的ListView搭配使用很愉快。
action,runInAction和嚴格模式(useStrict)
mobx推薦將修改被觀測變量的行為放在action中。
來看看以下例子:
import {observable, action} from 'mobx'; class Store { @observable number = 0; @action add = () => { this.number++; } } const newStore = new Store(); newStore.add();
以上例子使用了ES7的decorator,在實際開發中非常建議用上它,它可以給你帶來更多的便捷
好了回到我們的例子,這個類中有一個add函數,用來將number的值加1,也就是修改了被觀測的變量,根據規范,我們要在這里使用action來修飾這個add函數。
勇於動手的你也許會發現,就算我把@action去掉,程序還是可以運行呀。
class Store { @observable number = 0; add = () => { this.number++; } }
這是因為現在我們使用的Mobx的非嚴格模式,如果在嚴格模式下,就會報錯了。
接下來讓我們來啟用嚴格模式
import {observable, action, useStrict} from 'mobx'; useStrict(true); class Store { @observable number = 0; @action add = () => { this.number++; } } const newStore = new Store(); newStore.add();
嗯,Mobx里啟用嚴格模式的函數就是useStrict,注意和原生JS的"use strict"不是一個東西。
現在再去掉@action就會報錯了。
實際開發的時候建議開起嚴格模式,這樣不至於讓你在各個地方很輕易地區改變你所需要的值,降低不確定性。
action的寫法大概有如下幾種(摘自mobx英文文檔):
- action(fn)
- action(name, fn)
- @action classMethod() {}
- @action(name) classMethod () {}
- @action boundClassMethod = (args) => { body }
- @action(name) boundClassMethod = (args) => { body }
- @action.bound classMethod() {}
- @action.bound(function() {})
可以看到,action在修飾函數的同時,我們還可以給它設置一個name,這個name應該沒有什么太大的作用,但可以作為一個注釋更好地讓其他人理解這個action的意圖。
接下來說一個重點action只能影響正在運行的函數,而無法影響當前函數調用的異步操作
比如官網中給了如下例子
@action createRandomContact() {
this.pendingRequestCount++; superagent .get('https://randomuser.me/api/') .set('Accept', 'application/json') .end(action("createRandomContact-callback", (error, results) => { if (error) console.error(error); else { const data = JSON.parse(results.text).results[0]; const contact = new Contact(this, data.dob, data.name, data.login.username, data.picture); contact.addTag('random-user'); this.contacts.push(contact); this.pendingRequestCount--; } })); }
重點關注程序的第六行。在end中觸發的回調函數,被action給包裹了,這就很好驗證了上面加粗的那句話,action無法影響當前函數調用的異步操作,而這個回調毫無疑問是一個異步操作,所以必須再用一個action來包裹住它,這樣程序才不會報錯。。
當然如果你說是在非嚴格模式下……那當我沒說吧。。
如果你使用async function來處理業務,那么我們可以使用runInAction這個API來解決之前的問題。
import {observable, action, useStrict, runInAction} from 'mobx'; useStrict(true); class Store { @observable name = ''; @action load = async () => { const data = await getData(); runInAction(() => { this.name = data.name; }); } }
你可以把runInAction有點類似action(fn)()的語法糖,調用后,這個action方法會立刻執行。
結合React使用
在React中,我們一般會把和頁面相關的數據放到state中,在需要改變這些數據的時候,我們會去用setState這個方法來進行改變。
先設想一個最簡單的場景,頁面上有個數字0和一個按鈕。點擊按鈕我要讓這個數字增加1,就讓我們要用Mobx來處理這個試試。
import React from 'react'; import { observable, useStrict, action } from 'mobx'; import { observer } from 'mobx-react'; useStrict(true); class MyState { @observable num = 0; @action addNum = () => { this.num++; }; } const newState = new MyState(); @observer export default class App extends React.Component { render() { return ( <div> <p>{newState.num}</p> <button onClick={newState.addNum}>+1</button> </div> ) } }
上例中我們使用了一個MyState類,在這個類中定義了一個被觀測的num變量和一個action函數addNum來改變這個num值。
之后我們實例化一個對象,叫做newState,之后在我的React組件中,我只需要用@observer修飾一下組件類,便可以愉悅地使用這個newState對象中的值和函數了。
跨組件交互
在不使用其它框架、類庫的情況下,React要實現跨組件交互這一功能相對有些繁瑣。通常我們需要在父組件上定義一個state和一個修改該state的函數。然后把state和這個函數分別傳到兩個子組件里,在邏輯簡單,且子組件很少的時候可能還好,但當業務復雜起來后,這么寫就非常繁瑣,且難以維護。而用Mobx就可以很好地解決這個問題。來看看以下的例子:
class MyState {
@observable num1 = 0;
@observable num2 = 100;
@action addNum1 = () => {
this.num1 ++;
};
@action addNum2 = () => {
this.num2 ++;
};
@computed get total() {
return this.num1 + this.num2;
}
}
const newState = new MyState();
const AllNum = observer((props) => <div>num1 + num2 = {props.store.total}</div>); const Main = observer((props) => ( <div> <p>num1 = {props.store.num1}</p> <p>num2 = {props.store.num2}</p> <div> <button onClick={props.store.addNum1}>num1 + 1</button> <button onClick={props.store.addNum2}>num2 + 1</button> </div> </div> )); @observer export default class App extends React.Component { render() { return ( <div> <Main store={newState} /> <AllNum store={newState} /> </div> ); } }
有兩個子組件,Main和AllNum (均采用無狀態函數的方式聲明的組件)
在MyState中存放了這些組件要用到的所有狀態和函數。
之后只要在父組件需要的地方實例化一個MyState對象,需要用到數據的子組件,只需要將這個實例化的對象通過props傳下去就好了。
那如果組件樹比較深怎么辦呢?
我們可以借助React15版本的新特性context
來完成。它可以將父組件中的值傳遞到任意層級深度的子組件中。
詳情可以查看React的官方文檔 React context
接下來看看網絡請求的情況。
useStrict(true); class MyState { @observable data = null; @action initData = async() => { const data = await getData("xxx"); runInAction("說明一下這個action是干什么的。不寫也可以", () => { this.data = data; }) }; }
嚴格模式下,只能在action中修改數據,但是action只能影響到函數當前狀態下
的情景,也就是說在await之后發生的事情,這個action就修飾不到了,於是我們必須要使用了runInAction(詳細解釋見上文)。
當然如果你不開啟嚴格模式,不寫runInAction也不會報錯。
個人強烈建議開啟嚴格模式,這樣可以防止數據被任意修改,降低程序的不確定性
關於@observer的一些說明
通常,在和Mobx數據有關聯的時候,你需要給你的React組件加上@observer,你不必太擔心性能上的問題,加上這個@observer不會對性能產生太大的影響,而且@observer還有一個類似於pure render的功能,甚至能起到性能上的一些優化。
所謂pure render見下例:
@observer
export default class App extends React.Component { state = { a: 0, }; add = () => { this.setState({ a: this.state.a + 1 }); }; render() { return ( <div> {this.state.a} <button onClick={this.add}>+1</button> <PureItem /> </div> ); } } @observer class PureItem extends React.Component { render() { console.log('PureItem的render觸發了'); return ( <div>你們的事情跟我沒關系</div> ); } }
如果去掉子組件的@observer,按鈕每次點擊,控制台都會輸出 PureItem的render觸發了 這句話。
React組件中可以直接添加@observable修飾的變量
@observer
class MyComponent extends React.Component { state = { a: 0 }; @observable b = 1; render() { return( <div> {this.state.a} {this.b} </div> ) } }
在添加@observer后,你的組件會多一個生命周期componentWillReact
。當組件內被observable觀測的數據改變后,就會觸發這個生命周期。
注意setState並不會觸發這個生命周期!state中的數據和observable數據並不算是一類。
另外被observable觀測數據的修改是同步的,不像setState那樣是異步,這點給我們帶了很大便利。
Observable Object和Observable Arrays
本章主要對官方文檔Observable Types這一節中的前兩章進行了翻譯概述。有興趣的同學可以直接閱讀官方文章 Mobx官方文檔——Observable Types
Observable Objects
如果使用observable來修飾一個Javascript的簡單對象,那么其中的所有屬性都將變為可觀察的,如果其中某個屬性是對象或者數組,那么這個屬性也將被observable進行觀察,說白了就是遞歸調用。
Tips: 簡單對象是指不由構造函數創建,而是使用Object作為其原型,或是干脆沒有原型的對象。
需要注意,只有對象上已經存在的屬性,才能被observable所觀測到。
若是當時不存在,后續添加的屬性值,則需要使用extendObservable
來進行添加。
let observableObject = observable({value: 3222}); extendObservable(observableObject, { newValue: 2333 });
如果是由構造函數創建的對象,那么必須要再它的構造函數中使用observable或extendObservable來觀測對象。
如下所示:
function MyObject(name) { extendObservable(this, { name, }); } var obj = new MyObject("aaa");
如果對象中的屬性是由構造函數創建的對象,那么它也不會被observable給轉化。
對象中帶有getter修飾的屬性會被computed自動轉換。
其實observable函數的自動轉化已經能夠解決至少95%的問題了,如果想要更詳細地了解,可以去看 modifiers這一章
最后附一個購物車的例子
Observable Arrays
與對象類似,數組同樣可以使用observable函數進行轉化。
考慮到ES5中原生數組對象中存在一定的限制,所以Mobx將會創建一個類數組對象來代替原始數組。在實際使用中,這些類數組的表現和真正的原生數組極其類似,並且它支持原生數組的所有API,包括數組索引、長度獲取等。
但是注意一點,sort和reverse方法返回的是一個新的Observable Arrays,對原本的類數組不會產生影響,這一點和原生數組不一樣。
請記住,這個類數組不管和真實的數組有多么相似,它都不是一個真正的原生數組,所以毫無疑問Array.isArray(observable([]))的返回值都是false。當你需要將這個Observable Arrays轉換成真正的數組時,可以使用slice方法創建一個淺拷貝。換句話來說,Array.isArray(observable([]).slice())會返回true。
除了原生數組支持的API外,Observable Arrays還支持以下API:
-
intercept(interceptor)
這個方法可以在所有數組的操作被應用之前,將操作攔截。具體的請看Intercept & Observe -
observe(listener, fireImmediately? = false)
用來監聽數組的變化(類似ES7中的observe,可惜這個ES7中的observe將被廢棄),它返回一個用以注銷監聽器的函數。 -
clear()
清空數組 -
replace(newArray)
用一個新數組中的內容來替換掉原有的內容 -
find(predicate: (item, index, array) => boolean, thisArg?, fromIndex?)
基本上與ES7 Array.find的提案相同,不過多了fromIndex參數。 -
remove(value)
移除數組中第一個值等於value的元素,如果移除成功,則會返回true -
peek()
和slice類似,但它不會創建保護性拷貝,所以性能比slice會更好。如果你能夠確定,轉換出的數組肯定僅以只讀的方式使用,那么可以使用這個API
總結
Mobx想要入門上手可以說非常簡單,只需要記住少量概念並可以完成許多基礎業務了。但深入學習下去,也還是要接觸許多概念的。例如Modifier、Transation等等。
最后與Redux做一個簡單的對比
- Mobx寫法上更偏向於OOP
- 對一份數據直接進行修改操作,不需要始終返回一個新的數據
- 對typescript的支持更好一些
- 相關的中間件很少,邏輯層業務整合是一個問題
原文地址:https://www.jianshu.com/p/505d9d9fe36a