MobX

1. 介紹

1.1. 原理

React的render是 狀態 轉化為樹狀結構的渲染組件的方法
而MobX提供了一種存儲,更新 狀態 的方法
React 和 MobX都在優化著軟件開發中相同的問題。
React 使用的方法是讓虛擬DOM來減少繁瑣而沉重的DOM變化。
而MobX則通過一個虛擬的狀態依賴圖表來讓react組件和應用狀態同步化來減少不必要的狀態導致組件更新

1.2. 安裝

MobX:

npm install mobx --save

React bindings:

npm install mobx-react --save

1.3. 要點

MobX看起來很復雜的樣子,其實是用它只需要三步

  1. 定義你的狀態,讓它們成為觀察者(observable)
    存儲狀態(Store state)可以是任何的數據結構,隨你定義為:對象,數組,類,循環結構,引用都沒所謂。但需要記住一點,就是:隨著時間的變化,用MobX 去把它們定義成觀察者(observable)
import {observable} from 'mobx'
let appState = observable({
    timer: 0
})
  1. 我們不需要讓appState去觀察什么。你現在就能創建視圖(view),每當appState的相關數據發生變化的時候,就會自動更新。MobX會采用最優的方式去更新你的視圖。以下有一個例子來說明如何使用,其中使用了ES6/ES7的語法(當然MobX也是支持ES5),代碼中@的意義
import {observer} from 'mobx-react';
@observer
class TimerView extends React.Component {
    render() {
        return (<button onClick={this.onReset.bind(this)}>
                Seconds passed: {this.props.appState.timer}
            </button>);
    }
    onReset () {
        //appState.resetTimer會在下一節完成
        this.props.appState.resetTimer();
    }
};
React.render(<TimerView appState={appState} />, document.body);
  1. 修改狀態
    第三節要說的是修改狀態。MobX和其他框架不同,它不會要求你去做什么事情,它只是幫助你去做簡單的事情
appState.resetTimer = action(function reset() {
    appState.timer = 0;
});
setInterval(action(function tick() {
    appState.timer += 1;
}), 1000);

其中action包裝用法只能在strict模式下使用,請記得在你的javascript文件頭寫上:'use strict'。

2. API

從上面的例子可以看到,MobX的API其實不多:observable, computed, reactions, actions

2.1. observable(value)

其中的value可以是JS原定的數據結構,引用,對象,數組,ES6的map

  1. 如果value是一個map的話,則需要使用一個調節器(modifier)asMap來使用。這時候會返回一個Observable Map
  2. 如果是一個數組,返回Observable Array
  3. 如果是一個沒有屬性的對象,則返回一個Observable Object
  4. 如果是一個有屬性的對象,JS原有的數據結構,函數等,返回一個** Boxed Observable**。MobX不會自動讓一個有屬性的對象成為觀察者。這是這個有屬性的對象的構造函數應該做的事情,你可以使用extendObservable在它的構造函數里面,或者在它的類使用@observable去定義。

以下是一些例子:

const map = observable(asMap({ key: "value"}));
map.set("key", "new value");

const list = observable([1, 2, 4]);
list[2] = 3;

const person = observable({
    firstName: "Clive Staples",
    lastName: "Lewis"
});
person.firstName = "C.S.";

const temperature = observable(20);
temperature.set(25);

2.2. @observable

import {observable} from "mobx";
class OrderLine {
    @observable price:number = 0;
    @observable amount:number = 1;
    constructor(price) {
        this.price = price;
    }
    //這里在下一節會說到
    @computed get total() {
        return this.price * this.amount;
    }
}
const line = new OrderLine();
console.log("price" in line); // true
//hasOwnProperty:判斷一個對象是否有你給出名稱的屬性或對象。需要注意,此方法無法檢查該對象的原型鏈中是否具有該屬性
console.log(line.hasOwnProperty("price")); //false

如果你的環境不支持ES6/7的語法的話,其實@observable key = value; 只是extendObservable(this, { key: value })的語法糖。因此在ES5環境下你也能使用

2.3. (@)computed

Computed values 就像一個算術公式一樣去從現有的狀態或其他值去計算出需要的值。計算的耗費是不可低估的。Computed盡可能幫你減少其中的耗費。它們是高度優化的,請把它用在可能用到的地方。

不要混淆下一節說到的autorun。雖然他們都是被動調用的表達式。但是……
Computed使用情況:如果你需要產生一個有觀察者(observers)參數計算的新的值的時候
autorun使用情況:你不想產生一個新的值就想達到一個新的效果/功能。就像是打log或者進行網絡請求
Computed values是自動幫你從你的狀態(state)值和其他計算輔助值來計算的。MobX做了很多的優化。當參與計算的值沒有發生改變,Computed是不會重新運行。如果參與計算的值沒有被使用,Computed values是暫停的。

如果Computed values不再是觀察者(observed),那么在UI上也會把它除掉,MobX能自動做垃圾回收。autorun則需要你自己手動去處理。如果參與計算的值不再被使用,是不會緩存Computed的,所以重新計算是需要的。這個是最理想的默認情況。如果你想保留,可以了解一下keepalive和observe。

例子1: 在2.2的例子。@computed get

例子2: @computed set

class Foo {
    @observable length: 2,
    @computed get squared() {
        return this.length * this.length;
    }
    set squared(value) { //this is automatically an action, no annotation necessary
        this.length = Math.sqrt(value);
    }
}

需要注意的是:setter并非用于直接改變參數計算的值,如例子中的length。而是作為一個逆推導。

2.4. Autorun

Autorun是用在一些你想要產生一個不用觀察者參與的被動調用函數里面。當autorun被使用的時候,一旦依賴項發生變化,autorun提供的函數就會被執行。與之相反的是,computed提供的函數只會在他有自己的觀察員(observers)的時候才會評估是否重新執行,否則它的值被認為是無用的。

根據這些經驗:如果你需要一個自動運行但卻不會產生任何新的值的結果的函數,那么請使用Autorun。其他情況請使用computed。Autorun只是作用于如果達到某個效果或者功能,而不是計算某些值。如果有一個字符串作為第一個參數存入Autorun,那么它將成為一個調試名稱。

var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a, b) => a + b, 0));

var disposer = autorun(() => console.log(sum.get()));
// prints '6'
numbers.push(4);
// prints '10'

2.5. @observer

  1. observer 函數/修飾器用于react組件。通過mobx-react依賴包來提供。它通過mobx.autorun來包裝了組件的render函數,以確保組件的render函數在任何數據的更改是強制重新渲染
import {observer} from "mobx-react";
var timerData = observable({
    secondsPassed: 0
});
setInterval(() => {
    timerData.secondsPassed++;
}, 1000);
@observer class Timer extends React.Component {
    render() {
        return (<span>Seconds passed: { this.props.timerData.secondsPassed } </span> )
    }
});
React.render(<Timer timerData={timerData} />, document.body);

tips: 如果還有其他的decorators一起或者高階組件的存在,請確保observer為最內層(優先應用)的修飾器。否則它可能無法工作。如果你只在ES5的環境下工作:其實observer不過是observer(class Timer ... { }) 的語法糖。

  1. 難點—組件中相關值的引用:
    MobX能做的事情很多,但是它卻不能把原始的值變成觀察者(盡管可以通過包裹這個值來返回一個boxed observables的對象)。所以觀察者不是這個原始的值,而是返回后的對象的屬性值。修改一個剛才的例子:
React.render(<Timer timerData={timerData.secondsPassed} />, document.body)

這時候程序并不會工作了。傳入組件的只是timerData里面secondsPassed的當前值。在組件里面,它是不可變的。

  1. 把你的組件內部狀態變成可觀察的
    和普通的類一樣,你可以在你的組件使用@observable修飾器。這意味著你的組件擁有了一個內部state,而且它不需要使用react內部提供的繁瑣的setState機制。這個內部state能調起render函數,但是卻不能準確調起React的生命周期函數,例如:componentShouldUpdate / componentWillUpdate。如果你想要這些,最好使用react提供的API來創建state。當然也可以這樣寫
import {observer} from "mobx-react"
import {observable} from "mobx"
@observer class Timer extends React.Component {
    @observable secondsPassed = 0
    componentWillMount() {
        setInterval(() => {
            this.secondsPassed++
        }, 1000)
    }
    render() {
        return (<span>Seconds passed: { this.secondsPassed } </span> )
    }
})
React.render(<Timer />, document.body)
  1. 連接observer和stores
    mobx-react提供了Provider組件讓你可以把傳遞下來的stores作用在react提供的上下文機制。通過連接這些stores和observer,這些observer會成為組件的屬性來使用。
const colors = observable({
   foreground: '#000',
   background: '#fff'
});
const App = () =>
  <Provider colors={colors}>
     <app stuff... />
  </Provider>;
const Button = observer(["colors"], ({ colors, label, onClick }) =>
  <button style={{
      color: colors.foreground,
      backgroundColor: colors.background
    }}
    onClick={onClick}
  >{label}<button>
);
// later..
colors.foreground = 'blue';
// all buttons updated
  1. componentWillReact
    React 的組件總是從新的堆棧去渲染。因此讓它它很難判斷一個組件是否需要重新渲染。在mobx-react里面,你可以使用重新定義的生命周期componentWillReact。它只會在觀察者發生變化的時候才重新渲染。
import {observer} from "mobx-react";
@observer class TodoView extends React.Component {
    componentWillReact() {
        console.log("I will re-render, since the todo has changed!");
    }
    render() {
        return <div>this.props.todo.title</div>;
    }
}

componentWillReact沒有任何參數,而且不會在render初始化之前執行(componentWillMount的區別)。而當接收新的屬性或者setState之后,它會被調用。

2.6. action

  1. 任何應用程序都有操作(action)。action是任何改變狀態的事物。使用MobX,您可以通過標記它們在您的代碼中顯式地顯示您的操作(action)。它會更好的幫助你組織你的代碼。建議將它們用于修改可觀察量或具有副作用的任何函數中。
    需要注意的是:action是用在strict mode 中的
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 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--;
                 }
        }
))}
  1. action 僅僅作用于當前運行的函數,而不能作用于當前函數調用的函數。這意味著在一些定時器或者網絡請求,異步處理的情況下,它們的回調函數無法對狀態做成改變。這些回調函數都應該有action包裹,如果例子里面的 createRandomContact-callback 一樣。但是,如果你使用了async / await的話,最好的方式應該是使用 runInAction 來讓它變得更加簡單
@action /*optional*/ updateDocument = async () => {
    const data = await fetchDataFromUrl();
    /* required in strict mode to be allowed to update state: */
    runInAction("update state after fetching data", () => {
        this.data.replace(data);
        this.isSaving = true;
    })
}
  1. Bound actions
    目前看到的actions都是遵循在javascript中綁定的正常規則,但是在MobX 3引入了action.bound來自動綁定actions到目標對象上。和action的使用不一樣,不需要一個名字參數。它的名稱始終基于綁定到屬性的操作上。需要注意的是,在箭頭函數上不要這樣使用,因為箭頭函數已經綁定了上下文,不能在重新更改上下文
class Ticker {
    @observable this.tick = 0

    @action.bound
    increment() {
        this.tick++ // 'this' will always be correct
    }
}
const ticker = new Ticker()
setInterval(ticker.increment, 1000)

后記

啟動例子項目:進入

  1. 前端項目:npm install 或者 yarn install 打開本地的8080端口
  2. 后端部分:進入 back文件夾,執行npm start 打開本地的3000端口
  3. 作為對比:使用redux的例子:進入
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容