MobX

1. 介紹

1.1. 原理

React的render是 狀態(tài) 轉(zhuǎn)化為樹(shù)狀結(jié)構(gòu)的渲染組件的方法
而MobX提供了一種存儲(chǔ),更新 狀態(tài) 的方法
React 和 MobX都在優(yōu)化著軟件開(kāi)發(fā)中相同的問(wèn)題。
React 使用的方法是讓虛擬DOM來(lái)減少繁瑣而沉重的DOM變化。
而MobX則通過(guò)一個(gè)虛擬的狀態(tài)依賴圖表來(lái)讓react組件和應(yīng)用狀態(tài)同步化來(lái)減少不必要的狀態(tài)導(dǎo)致組件更新

1.2. 安裝

MobX:

npm install mobx --save

React bindings:

npm install mobx-react --save

1.3. 要點(diǎn)

MobX看起來(lái)很復(fù)雜的樣子,其實(shí)是用它只需要三步

  1. 定義你的狀態(tài),讓它們成為觀察者(observable)
    存儲(chǔ)狀態(tài)(Store state)可以是任何的數(shù)據(jù)結(jié)構(gòu),隨你定義為:對(duì)象,數(shù)組,類,循環(huán)結(jié)構(gòu),引用都沒(méi)所謂。但需要記住一點(diǎn),就是:隨著時(shí)間的變化,用MobX 去把它們定義成觀察者(observable)
import {observable} from 'mobx'
let appState = observable({
    timer: 0
})
  1. 我們不需要讓appState去觀察什么。你現(xiàn)在就能創(chuàng)建視圖(view),每當(dāng)appState的相關(guān)數(shù)據(jù)發(fā)生變化的時(shí)候,就會(huì)自動(dòng)更新。MobX會(huì)采用最優(yōu)的方式去更新你的視圖。以下有一個(gè)例子來(lái)說(shuō)明如何使用,其中使用了ES6/ES7的語(yǔ)法(當(dāng)然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會(huì)在下一節(jié)完成
        this.props.appState.resetTimer();
    }
};
React.render(<TimerView appState={appState} />, document.body);
  1. 修改狀態(tài)
    第三節(jié)要說(shuō)的是修改狀態(tài)。MobX和其他框架不同,它不會(huì)要求你去做什么事情,它只是幫助你去做簡(jiǎn)單的事情
appState.resetTimer = action(function reset() {
    appState.timer = 0;
});
setInterval(action(function tick() {
    appState.timer += 1;
}), 1000);

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

2. API

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

2.1. observable(value)

其中的value可以是JS原定的數(shù)據(jù)結(jié)構(gòu),引用,對(duì)象,數(shù)組,ES6的map

  1. 如果value是一個(gè)map的話,則需要使用一個(gè)調(diào)節(jié)器(modifier)asMap來(lái)使用。這時(shí)候會(huì)返回一個(gè)Observable Map
  2. 如果是一個(gè)數(shù)組,返回Observable Array
  3. 如果是一個(gè)沒(méi)有屬性的對(duì)象,則返回一個(gè)Observable Object
  4. 如果是一個(gè)有屬性的對(duì)象,JS原有的數(shù)據(jù)結(jié)構(gòu),函數(shù)等,返回一個(gè)** Boxed Observable**。MobX不會(huì)自動(dòng)讓一個(gè)有屬性的對(duì)象成為觀察者。這是這個(gè)有屬性的對(duì)象的構(gòu)造函數(shù)應(yīng)該做的事情,你可以使用extendObservable在它的構(gòu)造函數(shù)里面,或者在它的類使用@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;
    }
    //這里在下一節(jié)會(huì)說(shuō)到
    @computed get total() {
        return this.price * this.amount;
    }
}
const line = new OrderLine();
console.log("price" in line); // true
//hasOwnProperty:判斷一個(gè)對(duì)象是否有你給出名稱的屬性或?qū)ο蟆P枰⒁猓朔椒o(wú)法檢查該對(duì)象的原型鏈中是否具有該屬性
console.log(line.hasOwnProperty("price")); //false

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

2.3. (@)computed

Computed values 就像一個(gè)算術(shù)公式一樣去從現(xiàn)有的狀態(tài)或其他值去計(jì)算出需要的值。計(jì)算的耗費(fèi)是不可低估的。Computed盡可能幫你減少其中的耗費(fèi)。它們是高度優(yōu)化的,請(qǐng)把它用在可能用到的地方。

不要混淆下一節(jié)說(shuō)到的autorun。雖然他們都是被動(dòng)調(diào)用的表達(dá)式。但是……
Computed使用情況:如果你需要產(chǎn)生一個(gè)有觀察者(observers)參數(shù)計(jì)算的新的值的時(shí)候
autorun使用情況:你不想產(chǎn)生一個(gè)新的值就想達(dá)到一個(gè)新的效果/功能。就像是打log或者進(jìn)行網(wǎng)絡(luò)請(qǐng)求
Computed values是自動(dòng)幫你從你的狀態(tài)(state)值和其他計(jì)算輔助值來(lái)計(jì)算的。MobX做了很多的優(yōu)化。當(dāng)參與計(jì)算的值沒(méi)有發(fā)生改變,Computed是不會(huì)重新運(yùn)行。如果參與計(jì)算的值沒(méi)有被使用,Computed values是暫停的。

如果Computed values不再是觀察者(observed),那么在UI上也會(huì)把它除掉,MobX能自動(dòng)做垃圾回收。autorun則需要你自己手動(dòng)去處理。如果參與計(jì)算的值不再被使用,是不會(huì)緩存Computed的,所以重新計(jì)算是需要的。這個(gè)是最理想的默認(rèn)情況。如果你想保留,可以了解一下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并非用于直接改變參數(shù)計(jì)算的值,如例子中的length。而是作為一個(gè)逆推導(dǎo)。

2.4. Autorun

Autorun是用在一些你想要產(chǎn)生一個(gè)不用觀察者參與的被動(dòng)調(diào)用函數(shù)里面。當(dāng)autorun被使用的時(shí)候,一旦依賴項(xiàng)發(fā)生變化,autorun提供的函數(shù)就會(huì)被執(zhí)行。與之相反的是,computed提供的函數(shù)只會(huì)在他有自己的觀察員(observers)的時(shí)候才會(huì)評(píng)估是否重新執(zhí)行,否則它的值被認(rèn)為是無(wú)用的。

根據(jù)這些經(jīng)驗(yàn):如果你需要一個(gè)自動(dòng)運(yùn)行但卻不會(huì)產(chǎn)生任何新的值的結(jié)果的函數(shù),那么請(qǐng)使用Autorun。其他情況請(qǐng)使用computed。Autorun只是作用于如果達(dá)到某個(gè)效果或者功能,而不是計(jì)算某些值。如果有一個(gè)字符串作為第一個(gè)參數(shù)存入Autorun,那么它將成為一個(gè)調(diào)試名稱。

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 函數(shù)/修飾器用于react組件。通過(guò)mobx-react依賴包來(lái)提供。它通過(guò)mobx.autorun來(lái)包裝了組件的render函數(shù),以確保組件的render函數(shù)在任何數(shù)據(jù)的更改是強(qiáng)制重新渲染
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一起或者高階組件的存在,請(qǐng)確保observer為最內(nèi)層(優(yōu)先應(yīng)用)的修飾器。否則它可能無(wú)法工作。如果你只在ES5的環(huán)境下工作:其實(shí)observer不過(guò)是observer(class Timer ... { }) 的語(yǔ)法糖。

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

這時(shí)候程序并不會(huì)工作了。傳入組件的只是timerData里面secondsPassed的當(dāng)前值。在組件里面,它是不可變的。

  1. 把你的組件內(nèi)部狀態(tài)變成可觀察的
    和普通的類一樣,你可以在你的組件使用@observable修飾器。這意味著你的組件擁有了一個(gè)內(nèi)部state,而且它不需要使用react內(nèi)部提供的繁瑣的setState機(jī)制。這個(gè)內(nèi)部state能調(diào)起render函數(shù),但是卻不能準(zhǔn)確調(diào)起React的生命周期函數(shù),例如:componentShouldUpdate / componentWillUpdate。如果你想要這些,最好使用react提供的API來(lái)創(chuàng)建state。當(dāng)然也可以這樣寫
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組件讓你可以把傳遞下來(lái)的stores作用在react提供的上下文機(jī)制。通過(guò)連接這些stores和observer,這些observer會(huì)成為組件的屬性來(lái)使用。
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 的組件總是從新的堆棧去渲染。因此讓它它很難判斷一個(gè)組件是否需要重新渲染。在mobx-react里面,你可以使用重新定義的生命周期componentWillReact。它只會(huì)在觀察者發(fā)生變化的時(shí)候才重新渲染。
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沒(méi)有任何參數(shù),而且不會(huì)在render初始化之前執(zhí)行(componentWillMount的區(qū)別)。而當(dāng)接收新的屬性或者setState之后,它會(huì)被調(diào)用。

2.6. action

  1. 任何應(yīng)用程序都有操作(action)。action是任何改變狀態(tài)的事物。使用MobX,您可以通過(guò)標(biāo)記它們?cè)谀拇a中顯式地顯示您的操作(action)。它會(huì)更好的幫助你組織你的代碼。建議將它們用于修改可觀察量或具有副作用的任何函數(shù)中。
    需要注意的是: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 僅僅作用于當(dāng)前運(yùn)行的函數(shù),而不能作用于當(dāng)前函數(shù)調(diào)用的函數(shù)。這意味著在一些定時(shí)器或者網(wǎng)絡(luò)請(qǐng)求,異步處理的情況下,它們的回調(diào)函數(shù)無(wú)法對(duì)狀態(tài)做成改變。這些回調(diào)函數(shù)都應(yīng)該有action包裹,如果例子里面的 createRandomContact-callback 一樣。但是,如果你使用了async / await的話,最好的方式應(yīng)該是使用 runInAction 來(lái)讓它變得更加簡(jiǎn)單
@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中綁定的正常規(guī)則,但是在MobX 3引入了action.bound來(lái)自動(dòng)綁定actions到目標(biāo)對(duì)象上。和action的使用不一樣,不需要一個(gè)名字參數(shù)。它的名稱始終基于綁定到屬性的操作上。需要注意的是,在箭頭函數(shù)上不要這樣使用,因?yàn)榧^函數(shù)已經(jīng)綁定了上下文,不能在重新更改上下文
class Ticker {
    @observable this.tick = 0

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

后記

啟動(dòng)例子項(xiàng)目:進(jìn)入

  1. 前端項(xiàng)目:npm install 或者 yarn install 打開(kāi)本地的8080端口
  2. 后端部分:進(jìn)入 back文件夾,執(zhí)行npm start 打開(kāi)本地的3000端口
  3. 作為對(duì)比:使用redux的例子:進(jìn)入
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容