Mobx使用詳解

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 & Observe

  • observe(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的元素该溯,如果移除成功,則會返回true

  • peek()
    和slice類似别惦,但它不會創(chuàng)建保護性拷貝狈茉,所以性能比slice會更好。如果你能夠確定掸掸,轉(zhuǎn)換出的數(shù)組肯定僅以只讀的方式使用氯庆,那么可以使用這個API

總結(jié)

Mobx想要入門上手可以說非常簡單,只需要記住少量概念并可以完成許多基礎業(yè)務了扰付。但深入學習下去堤撵,也還是要接觸許多概念的。例如Modifier羽莺、Transation等等实昨。
最后與Redux做一個簡單的對比

  1. Mobx寫法上更偏向于OOP
  2. 對一份數(shù)據(jù)直接進行修改操作,不需要始終返回一個新的數(shù)據(jù)
  3. 對typescript的支持更好一些
  4. 相關(guān)的中間件很少盐固,邏輯層業(yè)務整合是一個問題
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荒给,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子闰挡,更是在濱河造成了極大的恐慌锐墙,老刑警劉巖礁哄,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件长酗,死亡現(xiàn)場離奇詭異,居然都是意外死亡桐绒,警方通過查閱死者的電腦和手機夺脾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茉继,“玉大人咧叭,你說我怎么就攤上這事∷附撸” “怎么了菲茬?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長派撕。 經(jīng)常有香客問我婉弹,道長,這世上最難降的妖魔是什么终吼? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任镀赌,我火速辦了婚禮,結(jié)果婚禮上际跪,老公的妹妹穿的比我還像新娘商佛。我一直安慰自己喉钢,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布良姆。 她就那樣靜靜地躺著肠虽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玛追。 梳的紋絲不亂的頭發(fā)上舔痕,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音豹缀,去河邊找鬼伯复。 笑死,一個胖子當著我的面吹牛邢笙,可吹牛的內(nèi)容都是我干的啸如。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氮惯,長吁一口氣:“原來是場噩夢啊……” “哼叮雳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起妇汗,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帘不,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后杨箭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寞焙,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年互婿,在試婚紗的時候發(fā)現(xiàn)自己被綠了捣郊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡慈参,死狀恐怖呛牲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驮配,我是刑警寧澤娘扩,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站壮锻,受9級特大地震影響琐旁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜躯保,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一旋膳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧途事,春花似錦验懊、人聲如沸擅羞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽减俏。三九已至,卻和暖如春碱工,著一層夾襖步出監(jiān)牢的瞬間娃承,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工怕篷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留历筝,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓廊谓,卻偏偏與公主長得像梳猪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒸痹,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容