高效的Mobx模式(Part 3 高階應用實例)

前兩部分側重於MobX的基本構建塊。 有了這些塊,咱們如今能夠經過MobX的角度開始解決一些真實場景。 這篇文章將是一系列應用咱們迄今爲止所見概念的例子。javascript

固然,這不是一個詳盡的清單,但應該讓你體會到應用MobX角度所須要的心理轉變。 全部示例都是在沒有@decorator (裝飾器)語法的狀況下建立的。 這容許您在Chrome控制檯,Node REPL或支持臨時文件的WebStorm等IDE中進行嘗試。java

改變思惟方式

當您學習某些庫或框架背後的理論並嘗試將其應用於您本身的問題時,您最初可能會畫一個空白。 它發生在像我這樣的普通人身上,甚至是最好的人。 寫做界稱之爲「Writer’s block」,而在藝術家的世界裏,它就是「Painter’s block」react

咱們須要的是從簡單到複雜的例子來塑造咱們的思惟方式。 只有看到應用程序,咱們才能開始想象解決咱們本身的問題的方法。segmentfault

對於MobX,它首先要了解您有一個reactive object-graph這一事實。 樹的某些部分可能依賴於其餘部分。 當樹變異時,鏈接的部分將做出反應並更新以反映變化。數組

思惟方式的轉變是將手頭的系統設想爲一組反應性變更 + 一組相應的結果。

效果能夠是因爲反應性變化而產生輸出的任何事物。 讓咱們探索各類現實世界的例子,看看咱們如何用MobX建模和表達它們。
promise


Example 1: 發送重要操做的分析

問題:咱們在應用程序中有一些必須記錄到服務器的一次性操做。 咱們但願跟蹤執行這些操做的時間併發送分析。緩存

一、是對創建狀態模型。 咱們的行爲是有限的,咱們只關心它執行一次。 咱們可使用動做方法的名稱創建對應的布爾值類型狀態, 這是咱們可觀察到的狀態。服務器

const actionMap = observable({
    login: false,
    logout: false,
    forgotPassword: false,
    changePassword: false,
    loginFailed: false
});

二、接下來,咱們必須對這些行動狀態發生的變化做出反應。 由於它們只在生命週期中發生過一次,因此咱們不會使用長期運行的效果,如autorun()reaction()。 咱們也不但願這些效果在執行後存在。 好吧,這給咱們留下了一個選擇:....
....
....react-router

when

Object.keys(actionMap)
    .forEach(key => {
        when(
            () => actionMap[key],
            () => reportAnalyticsForAction(key)
        );
    });

function reportAnalyticsForAction(actionName) {
    console.log('Reporting: ', actionName);

    /* ... JSON API Request ... */
}

在上面的代碼中,咱們只是循環遍歷actionMap中的鍵併爲每一個鍵設置when()反作用。 當tracker-function(第一個參數)返回true時,反作用將運行。 運行效果函數(第二個參數)後,when()將自動處理。 所以,沒有從應用程序發送多個報告的問題!併發

三、咱們還須要一個MobX動做來改變可觀察狀態。 請記住:永遠不要直接修改您的observable。 始終經過action來作到這一點。
對上面的例子來講,以下:

const markActionComplete = action((name) => {
    actionMap[name] = true;
});

markActionComplete('login');
markActionComplete('logout');

markActionComplete('login');

// [LOG] Reporting:  login
// [LOG] Reporting:  logout

請注意,即便我將登陸操做標記觸發兩次,也沒有發送日誌報告。 完美,這正是咱們須要的結果。
它有兩個緣由:

  1. login標記已經爲true,所以值沒有變化
  2. 此外,when()反作用已被觸發執行,所以再也不發生追蹤。


Example 2: 做爲工做流程的一部分啓動操做

問題:咱們有一個由幾個狀態組成的工做流程。 每一個狀態都映射到某些任務,這些任務在工做流到達該狀態時執行。

一、從上面的描述中能夠看出,惟一可觀察的值是工做流的狀態。 須要爲每一個狀態運行的任務能夠存儲爲簡單映射。 有了這個,咱們能夠模擬咱們的工做流程:

class Workflow {
    constructor(taskMap) {
        this.taskMap = taskMap;
        this.state = observable({
            previous: null,
            next: null
        });

        this.transitionTo = action((name) => {
            this.state.previous = this.state.next;
            this.state.next = name;
        });

        this.monitorWorkflow();
    }

    monitorWorkflow() {
        /* ... */
    }
}

// Usage
const workflow = new Workflow({
    start() {
        console.log('Running START');
    },

    process(){
        console.log('Running PROCESS');
    },

    approve() {
        console.log('Running APPROVE');
    },

    finalize(workflow) {
        console.log('Running FINALIZE');

        setTimeout(()=>{
            workflow.transitionTo('end');
        }, 500);
    },

    end() {
        console.log('Running END');
    }
});

請注意,咱們正在存儲一個名爲state的實例變量,該變量跟蹤工做流的當前和先前狀態。 咱們還傳遞state->task的映射,存儲爲taskMap

2如今有趣的部分是關於監控工做流程。 在這種狀況下,咱們沒有像前一個例子那樣的一次性操做。 工做流一般是長時間運行的,可能在應用程序的生命週期內。 這須要autorunreaction()

只有在轉換到狀態時纔會執行狀態任務。 所以咱們須要等待對this.state.next進行更改才能運行任何反作用(任務)。 等待更改表示使用reaction()由於它僅在跟蹤的可觀察值更改值時纔會運行。 因此咱們的監控代碼以下所示:

class Workflow {
    /* ... */
    monitorWorkflow() {
        reaction(
            () => this.state.next,
            (nextState) => {
                const task = this.taskMap[nextState];
                if (task) {
                    task(this);
                }
            }
        )
    }
}

reaction()第一個參數是跟蹤函數,在這種狀況下只返回this.state.next。 當跟蹤功能的返回值改變時,它將觸發效果功能。 效果函數查看當前狀態,從this.taskMap查找任務並簡單地調用它。

請注意,咱們還將工做流的實例傳遞給任務。 這可用於將工做流轉換爲其餘狀態。

workflow.transitionTo('start');

workflow.transitionTo('finalize');

// [LOG] Running START
// [LOG] Running FINALIZE
/* ... after 500ms ... */
// [LOG] Running END

有趣的是,這種存儲一個簡單的observable的技術,好比this.state.next和使用reaction()來觸發反作用,也能夠用於:

  1. 經過react-router進行路由
  2. 在演示應用程序中導航
  3. 基於模式在不一樣視圖之間切換


Example 3: 輸入更改時執行表單驗證

問題:這是一個經典的Web表單用例,您須要驗證一堆輸入。 若是有效,容許提交表單。

一、讓咱們用一個簡單的表單數據類對其進行建模,其字段必須通過驗證。

class FormData {
    constructor() {
        extendObservable(this, {
            firstName: '',
            lastName: '',
            email: '',
            acceptTerms: false,

            errors: {},

            get valid() { // this becomes a computed() property
                return (this.errors === null);
            }
        });

        this.setupValidation(); // We will look at this below
    }
}

extendObservable()API是咱們之前從未見過的。 經過在咱們的類實例(this)上應用它,咱們獲得一個ES5至關於建立一個@observable類屬性。

class FormData {
    @observable firstName = '';
    /* ... */
}

二、接下來,咱們須要監視這些字段什麼時候發生變化並運行一些驗證邏輯。 若是驗證經過,咱們能夠將實體標記爲有效並容許提交。 使用計算屬性跟蹤有效性自己:有效。

因爲驗證邏輯須要在FormData的生命週期內運行,所以咱們將使用autorun()。 咱們也可使用reaction()但咱們想當即運行驗證而不是等待第一次更改。

class FormData {
    setupValidation() {
        autorun(() => {
            // Dereferencing observables for tracking
            const {firstName, lastName, email, acceptTerms} = this;
            const props = {
                firstName,
                lastName,
                email,
                acceptTerms
            };

            this.runValidation(props, {/* ... */})
                .then(result => {
                    this.errors = result;
                })
        });
    }

    runValidation(propertyMap, rules) {
        return new Promise((resolve) => {
            const {firstName, lastName, email, acceptTerms} = propertyMap;

            const isValid = (firstName !== '' && lastName !== '' && email !== '' && acceptTerms === true);
            resolve(isValid ? null : {/* ... map of errors ... */});
        });
    }

}

在上面的代碼中,autorun()將在跟蹤的observables發生更改時自動觸發。 請注意,要使MobX正確跟蹤您的observable,您必須使用解除引用。

runValidation()是一個異步調用,這就是咱們返回一個promise的緣由。 在上面的示例中,它並不重要,但在現實世界中,您可能會調用服務器進行一些特殊驗證。 當結果返回時,咱們將設置錯誤observable,這將反過來更新有效的計算屬性。

若是你有一個耗時較大的驗證邏輯,你甚至可使用autorunAsync(),它有一個參數能夠延遲執行去抖動。

二、好吧,讓咱們的代碼付諸行動。 咱們將設置一個簡單的控制檯記錄器(經過autorun())並跟蹤有效的計算屬性。

const instance = new FormData();

// Simple console logger
autorun(() => {
    // input的每一次輸入,結果都會觸發error變動,autorun隨即執行
    const validation = instance.errors;

    console.log(`Valid = ${instance.valid}`);
    if (instance.valid) {
        console.log('--- Form Submitted ---');
    }

});

// Let's change the fields
instance.firstName = 'Pavan';
instance.lastName = 'Podila';
instance.email = 'pavan@pixelingene.com';
instance.acceptTerms = true;

//     輸出日誌以下
//     Valid = false
//    Valid = false
//    Valid = false
//    Valid = false
//    Valid = false
//    Valid = true
//    --- Form Submitted ---

因爲autonrun()當即運行,您將在開頭看到兩個額外的日誌,一個用於instance.errors,一個用於instance.valid,第1-2行。 其他四行(3-6)用於現場的每次更改。

每一個字段更改都會觸發runValidation(),每次都會在內部返回一個新的錯誤對象。 這會致使instance.errors的引用發生更改,而後觸發咱們的autorun()以記錄有效標誌。 最後,當咱們設置了全部字段時,instance.errors變爲null(再次更改引用)並記錄最終的「Valid = true」。

四、簡而言之,咱們經過使表單字段可觀察來進行表單驗證。 咱們還添加了額外的errors屬性和有效的計算屬性來跟蹤有效性。 autorun()經過將全部內容捆綁在一塊兒來節省時間。


Example 4: 跟蹤全部已註冊的組件是否已加載

問題: 咱們有一組已註冊的組件,咱們但願在全部組件都加載後跟蹤。 每一個組件都將公開一個返回 promise的load()方法。 若是promise解析,咱們將組件標記爲已加載。 若是它拒絕,咱們將其標記爲失敗。 當全部這些都完成加載時,咱們將報告整個集是否已加載或失敗。

一、咱們先來看看咱們正在處理的組件。 咱們正在建立一組隨機報告其負載狀態的組件。 另請注意,有些是異步的。

const components = [
    {
        name: 'first',
        load() {
            return new Promise((resolve, reject) => {
                Math.random() > 0.5 ? resolve(true) : reject(false);
            });
        }
    },
    {
        name: 'second',
        load() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    Math.random() > 0.5 ? resolve(true) : reject(false);
                }, 1000);
            });
        }
    },
    {
        name: 'third',
        load() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    Math.random() > 0.25 ? resolve(true) : reject(false);
                }, 500);
            });
        }
    },
];

二、下一步是爲Tracker設計可觀察狀態。 組件的load()不會按特定順序完成。 因此咱們須要一個可觀察的數組來存儲每一個組件的加載狀態。 咱們還將跟蹤每一個組件的報告狀態。

當全部組件都已報告時,咱們能夠通知組件集的最終加載狀態。 如下代碼設置了可觀察量。

class Tracker {
    constructor(components) {
        this.components = components;

        extendObservable(this, {

            // Create an observable array of state objects,
            // one per component
            states: components.map(({name}) => {
                return {
                    name,
                    reported: false,
                    loaded: undefined
                };
            }),

            // computed property that derives if all components have reported
            get reported() {
                return this.states.reduce((flag, state) => {
                    return flag && state.reported;
                }, true);
            },

            // computed property that derives the final loaded state 
            // of all components
            get loaded() {
                return this.states.reduce((flag, state) => {
                    return flag && !!state.loaded;
                }, true);
            },

            // An action method to mark reported + loaded
            mark: action((name, loaded) => {
                const state = this.states.find(state => state.name === name);

                state.reported = true;
                state.loaded = loaded;
            })

        });

    }
}

咱們回到使用extendObservable()來設置咱們的可觀察狀態。 reportedload的計算屬性跟蹤組件完成其加載的時間。 mark()是咱們改變可觀察狀態的動做方法。

順便說一句,建議在須要從您的observables派生值的任何地方使用computed。 將其視爲產生價值的可觀察物。 計算值也會被緩存,從而提升性能。 另外一方面,autorunreaction不會產生價值。 相反,它們提供了建立反作用的命令層。

三、爲了啓動跟蹤,咱們將在Tracker上建立一個track()方法。 這將觸發每一個組件的load()並等待返回的Promise解析/拒絕。 基於此,它將標記組件的負載狀態。

when()全部組件都已reported時,跟蹤器能夠報告最終加載的狀態。 咱們在這裏使用,由於咱們正在等待條件變爲真(this.reported)。 報告的反作用只須要發生一次,很是適合when()

如下代碼負責以上事項:

class Tracker {
    /* ... */ 
    track(done) {
        when(
            () => this.reported,
            () => {
                done(this.loaded);
            }
        );

        this.components.forEach(({name, load}) => {
            load()
                .then(() => {
                    this.mark(name, true);
                })
                .catch(() => {
                    this.mark(name, false);
                });
        });
    }

    setupLogger() {
        autorun(() => {
            const loaded = this.states.map(({name, loaded}) => {
                return `${name}: ${loaded}`;
            });

            console.log(loaded.join(', '));
        });
    }
}

setupLogger()實際上不是解決方案的一部分,但用於記錄報告。 這是瞭解咱們的解決方案是否有效的好方法。

四、如今咱們來測試一下:

const t = new Tracker(components);
t.setupLogger();
t.track((loaded) => {
    console.log('All Components Loaded = ', loaded);
});

// first: undefined, second: undefined, third: undefined
// first: true, second: undefined, third: undefined
// first: true, second: undefined, third: true
// All Components Loaded =  false
// first: true, second: false, third: true

記錄的輸出顯示其按預期工做。 在組件報告時,咱們記錄每一個組件的當前加載狀態。 當全部人報告時,this.reported變爲true,咱們看到「All Components Loaded」消息。

但願上面的一些例子讓你體會到在MobX中的思考。

  1. 設計可觀察狀態
  2. 設置變異動做方法以更改可觀察狀態
  3. 放入跟蹤功能(when,autorun,reaction)以響應可觀察狀態的變化

上述公式應該適用於須要在發生變化後跟蹤某些內容的複雜場景,這可能致使重複1-3步驟。

相關文章
相關標籤/搜索