Mobx是一個功能強大望几,上手非常容易的狀態(tài)管理工具。就連redux的作者也曾經(jīng)向大家推薦過它弱睦,在不少情況下你的確可以使用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可以用來觀測一個數(shù)據(jù)沟蔑,這個數(shù)據(jù)可以數(shù)字湿诊、字符串、數(shù)組瘦材、對象等類型(相關(guān)知識點具體會在后文中詳述)厅须,而當觀測到的數(shù)據(jù)發(fā)生變化的時候,如果變化的值處在autorun中食棕,那么autorun就會自動執(zhí)行朗和。
上例中的autorun函數(shù)中,只對value值進行了操作簿晓,而并沒有number值的什么事兒眶拉,所以number.set(101)
這步并不會觸發(fā)autorun,只有value的變化才觸發(fā)了autorun憔儿。
計算屬性——computed
假如現(xiàn)在我們一個數(shù)字镀层,但我們對它的值不感興趣,而只關(guān)心這個數(shù)組是否為正數(shù)皿曲。這個時候我們就可以用到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改變?yōu)?19痪枫,輸出false,也沒有問題叠艳。
但是當-19改變?yōu)?1的時候奶陈,雖然number變了,但是number的改變實際上并沒有改變plus的值附较,所以沒有其它地方收到通知吃粒,因此也就并沒有輸出任何值。
直到number重新變?yōu)?時才輸出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臀脏,在實際開發(fā)中非常建議用上它,它可以給你帶來更多的便捷
好了回到我們的例子,這個類中有一個add函數(shù)揉稚,用來將number的值加1秒啦,也就是修改了被觀測的變量,根據(jù)規(guī)范窃植,我們要在這里使用action來修飾這個add函數(shù)帝蒿。
勇于動手的你也許會發(fā)現(xiàn)荐糜,就算我把@action去掉巷怜,程序還是可以運行呀。
class Store {
@observable number = 0;
add = () => {
this.number++;
}
}
這是因為現(xiàn)在我們使用的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里啟用嚴格模式的函數(shù)就是useStrict关带,注意和原生JS的"use strict"不是一個東西。
現(xiàn)在再去掉@action就會報錯了沼撕。
實際開發(fā)的時候建議開起嚴格模式宋雏,這樣不至于讓你在各個地方很輕易地區(qū)改變你所需要的值,降低不確定性务豺。
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在修飾函數(shù)的同時,我們還可以給它設置一個name笼沥,這個name應該沒有什么太大的作用蚪燕,但可以作為一個注釋更好地讓其他人理解這個action的意圖。
接下來說一個重點action只能影響正在運行的函數(shù)奔浅,而無法影響當前函數(shù)調(diào)用的異步操作
比如官網(wǎng)中給了如下例子
@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--;
}
}));
}
重點關(guān)注程序的第六行馆纳。在end中觸發(fā)的回調(diào)函數(shù),被action給包裹了汹桦,這就很好驗證了上面加粗的那句話鲁驶,action無法影響當前函數(shù)調(diào)用的異步操作,而這個回調(diào)毫無疑問是一個異步操作舞骆,所以必須再用一個action來包裹住它灵嫌,這樣程序才不會報錯。葛作。
當然如果你說是在非嚴格模式下……那當我沒說吧寿羞。。
如果你使用async function來處理業(yè)務赂蠢,那么我們可以使用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)()的語法糖,調(diào)用后,這個action方法會立刻執(zhí)行玖院。
結(jié)合React使用
在React中菠红,我們一般會把和頁面相關(guān)的數(shù)據(jù)放到state中,在需要改變這些數(shù)據(jù)的時候难菌,我們會去用setState這個方法來進行改變试溯。
先設想一個最簡單的場景,頁面上有個數(shù)字0和一個按鈕郊酒。點擊按鈕我要讓這個數(shù)字增加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函數(shù)addNum來改變這個num值摹闽。
之后我們實例化一個對象,叫做newState褐健,之后在我的React組件中付鹿,我只需要用@observer修飾一下組件類,便可以愉悅地使用這個newState對象中的值和函數(shù)了蚜迅。
跨組件交互
在不使用其它框架舵匾、類庫的情況下,React要實現(xiàn)跨組件交互這一功能相對有些繁瑣谁不。通常我們需要在父組件上定義一個state和一個修改該state的函數(shù)坐梯。然后把state和這個函數(shù)分別傳到兩個子組件里,在邏輯簡單拍谐,且子組件很少的時候可能還好烛缔,但當業(yè)務復雜起來后,這么寫就非常繁瑣轩拨,且難以維護践瓷。而用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 (均采用無狀態(tài)函數(shù)的方式聲明的組件)
在MyState中存放了這些組件要用到的所有狀態(tài)和函數(shù)晕翠。
之后只要在父組件需要的地方實例化一個MyState對象,需要用到數(shù)據(jù)的子組件砍濒,只需要將這個實例化的對象通過props傳下去就好了淋肾。
那如果組件樹比較深怎么辦呢?
我們可以借助React15版本的新特性context
來完成爸邢。它可以將父組件中的值傳遞到任意層級深度的子組件中樊卓。
詳情可以查看React的官方文檔 React context
接下來看看網(wǎng)絡請求的情況。
useStrict(true);
class MyState {
@observable data = null;
@action initData = async() => {
const data = await getData("xxx");
runInAction("說明一下這個action是干什么的杠河。不寫也可以", () => {
this.data = data;
})
};
}
嚴格模式下碌尔,只能在action中修改數(shù)據(jù)浇辜,但是action只能影響到函數(shù)當前狀態(tài)下
的情景,也就是說在await之后發(fā)生的事情唾戚,這個action就修飾不到了柳洋,于是我們必須要使用了runInAction(詳細解釋見上文)。
當然如果你不開啟嚴格模式叹坦,不寫runInAction也不會報錯熊镣。
個人強烈建議開啟嚴格模式,這樣可以防止數(shù)據(jù)被任意修改募书,降低程序的不確定性
關(guān)于@observer的一些說明
通常绪囱,在和Mobx數(shù)據(jù)有關(guān)聯(lián)的時候,你需要給你的React組件加上@observer锐膜,你不必太擔心性能上的問題毕箍,加上這個@observer不會對性能產(chǎn)生太大的影響弛房,而且@observer還有一個類似于pure render的功能道盏,甚至能起到性能上的一些優(yōu)化。
所謂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觸發(fā)了');
return (
<div>你們的事情跟我沒關(guān)系</div>
);
}
}
如果去掉子組件的@observer文捶,按鈕每次點擊荷逞,控制臺都會輸出 PureItem的render觸發(fā)了 這句話。
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
种远。當組件內(nèi)被observable觀測的數(shù)據(jù)改變后,就會觸發(fā)這個生命周期顽耳。
注意setState并不會觸發(fā)這個生命周期坠敷!state中的數(shù)據(jù)和observable數(shù)據(jù)并不算是一類。
另外被observable觀測數(shù)據(jù)的修改是同步的射富,不像setState那樣是異步膝迎,這點給我們帶了很大便利。
Observable Object和Observable Arrays
本章主要對官方文檔Observable Types這一節(jié)中的前兩章進行了翻譯概述胰耗。有興趣的同學可以直接閱讀官方文章 Mobx官方文檔——Observable Types
Observable Objects
如果使用observable來修飾一個Javascript的簡單對象限次,那么其中的所有屬性都將變?yōu)榭捎^察的,如果其中某個屬性是對象或者數(shù)組柴灯,那么這個屬性也將被observable進行觀察卖漫,說白了就是遞歸調(diào)用。
Tips: 簡單對象是指不由構(gòu)造函數(shù)創(chuàng)建赠群,而是使用Object作為其原型羊始,或是干脆沒有原型的對象。
需要注意查描,只有對象上已經(jīng)存在的屬性突委,才能被observable所觀測到速警。
若是當時不存在,后續(xù)添加的屬性值鸯两,則需要使用extendObservable
來進行添加闷旧。
let observableObject = observable({value: 3222});
extendObservable(observableObject, {
newValue: 2333
});
如果是由構(gòu)造函數(shù)創(chuàng)建的對象,那么必須要再它的構(gòu)造函數(shù)中使用observable或extendObservable來觀測對象钧唐。
如下所示:
function MyObject(name) {
extendObservable(this, {
name,
});
}
var obj = new MyObject("aaa");
如果對象中的屬性是由構(gòu)造函數(shù)創(chuàng)建的對象忙灼,那么它也不會被observable給轉(zhuǎn)化。
對象中帶有g(shù)etter修飾的屬性會被computed自動轉(zhuǎn)換钝侠。
其實observable函數(shù)的自動轉(zhuǎn)化已經(jīng)能夠解決至少95%的問題了该园,如果想要更詳細地了解,可以去看 modifiers這一章
最后附一個購物車的例子
Observable Arrays
與對象類似帅韧,數(shù)組同樣可以使用observable函數(shù)進行轉(zhuǎn)化里初。
考慮到ES5中原生數(shù)組對象中存在一定的限制,所以Mobx將會創(chuàng)建一個類數(shù)組對象來代替原始數(shù)組忽舟。在實際使用中双妨,這些類數(shù)組的表現(xiàn)和真正的原生數(shù)組極其類似,并且它支持原生數(shù)組的所有API叮阅,包括數(shù)組索引刁品、長度獲取等。
但是注意一點浩姥,sort和reverse方法返回的是一個新的Observable Arrays挑随,對原本的類數(shù)組不會產(chǎn)生影響,這一點和原生數(shù)組不一樣勒叠。
請記住兜挨,這個類數(shù)組不管和真實的數(shù)組有多么相似,它都不是一個真正的原生數(shù)組眯分,所以毫無疑問Array.isArray(observable([]))的返回值都是false拌汇。當你需要將這個Observable Arrays轉(zhuǎn)換成真正的數(shù)組時,可以使用slice方法創(chuàng)建一個淺拷貝颗搂。換句話來說担猛,Array.isArray(observable([]).slice())會返回true。
除了原生數(shù)組支持的API外丢氢,Observable Arrays還支持以下API:
intercept(interceptor)
這個方法可以在所有數(shù)組的操作被應用之前傅联,將操作攔截。具體的請看Intercept & Observeobserve(listener, fireImmediately? = false)
用來監(jiān)聽數(shù)組的變化(類似ES7中的observe疚察,可惜這個ES7中的observe將被廢棄)蒸走,它返回一個用以注銷監(jiān)聽器的函數(shù)。clear()
清空數(shù)組replace(newArray)
用一個新數(shù)組中的內(nèi)容來替換掉原有的內(nèi)容find(predicate: (item, index, array) => boolean, thisArg?, fromIndex?)
基本上與ES7 Array.find的提案相同貌嫡,不過多了fromIndex參數(shù)比驻。remove(value)
移除數(shù)組中第一個值等于value的元素该溯,如果移除成功,則會返回truepeek()
和slice類似别惦,但它不會創(chuàng)建保護性拷貝狈茉,所以性能比slice會更好。如果你能夠確定掸掸,轉(zhuǎn)換出的數(shù)組肯定僅以只讀的方式使用氯庆,那么可以使用這個API
總結(jié)
Mobx想要入門上手可以說非常簡單,只需要記住少量概念并可以完成許多基礎業(yè)務了扰付。但深入學習下去堤撵,也還是要接觸許多概念的。例如Modifier羽莺、Transation等等实昨。
最后與Redux做一個簡單的對比
- Mobx寫法上更偏向于OOP
- 對一份數(shù)據(jù)直接進行修改操作,不需要始終返回一個新的數(shù)據(jù)
- 對typescript的支持更好一些
- 相關(guān)的中間件很少盐固,邏輯層業(yè)務整合是一個問題