高性能 MobX 模式(part 3)- 用例教程

譯者:阿里雲前端-也樹

原文鏈接:

  • Part 1 - Shaping the observables
  • Part 2 - Reacting to changes
  • Part 3 - A Cookbook of use cases

Part1譯文參考:高性能MobX模式(Part 1)

前面兩部分把重點放在了 MobX 基礎模塊的構建上。用這些模塊我們可以開始解決一些現實場景的問題了。這篇文章將會通過一系列的示例來應用我們已經了解的概念。

當然了,這不會是一個冗長的列表,而是可以讓你嘗試轉變思維去應用 MobX。所有示例都沒有使用 @decorator 的語法來實現。這樣可以讓你在 Chrome 控制台、Node命令行環境或者是像 Webstrom 這樣支持臨時文件的 IDE 中嘗試。

沒有概述(TLDR)?

這是一篇長文。很抱歉,沒有概述。這裡有四個示例,我覺得第二個示例之後,後面的閱讀起來會更快也更容易理解。:-)

  1. 為重要的動作發送數據分析
  2. 作為工作流的一部分來觸發操作
  3. 表單內容變化時顯示驗證信息
  4. 追蹤是否所有已註冊的組件完成載入

思維做出一些轉變

當你學習某個庫或框架背後的理論知識並且嘗試解決你自己的問題時,你大概會先初始化一個空白項目。像我這樣的一般人還有甚至說最棒的一些開發者都會這樣。

我們需要的是從簡單到複雜的示例,從而使我們的思維方式成型。只有當我們在實際應用中,我們才能開始思考自己問題的解決方法。

對於 MobX 來說,你最先需要理解的是你有一個響應式的對象數據表。在這個樹形結構上,某些部分會依賴另一些部分。當這個樹形結構發生突變,為了反應這些變化,有聯繫的部分就會響應並且更新。

思維的轉變在於想像整個系統是由一系列的突變和一系列相應的反應作用組成的。

作為響應式的變化的結果,產生的作用可以是能產出輸出的任何東西。讓我們去探索一些真實的示例並且看看我們如何使用 MobX 對它們進行建模和描述。

示例1:為重要的動作發送數據分析

問題: 我們在應用中有某些一次性的操作,需要在伺服器記錄下來。當這些動作被觸發並且發送數據分析時,我們想要去追蹤它們。

解決辦法

第一步是去給狀態建模。我們的操作是受限的,同時我們只在乎它什麼時候第一次被觸發。我們可以通過一個動作名稱-布爾值的 map 結構建立模型。下面是我們觀察的狀態。

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

接下來我們需要對發生在這些動作狀態上的變化做出響應。既然他們在整個生命周期中只發生一次,我們就不準備使用像 autorun() 和 reaction() 這樣長期運行的作用函數。我們同樣也不想這些動作在執行後保留產生的作用。那麼,留著我們的就只有一個選擇了:when。

Object.keys(actionMap)n .forEach(key => {n when(n () => actionMap[key],n () => reportAnalyticsForAction(key)n );n });nnfunction reportAnalyticsForAction(actionName) {n console.log(Reporting: , actionName);nn /* ... JSON API Request ... */n}n

在上面的代碼中,我們簡單的遍歷了我們 actionMap 中的 key 並且給每個 key 都設置了一個 when() 方法來處理副作用。這些副作用在追蹤函數(第一個參數)返回 true 的時候執行。在執行作用函數(第二個參數)之後,when() 方法會自動銷毀。所以不會存在應用中發送多次報告的問題。

我們還需要一個 MobX 的 action 來改變被觀察的狀態。記住:永遠不要直接修改被觀察的變數,使用 action() 來做這件事。

對我們這個問題來說,代碼會像下面這樣:

const markActionComplete = action((name) => {n actionMap[name] = true;n});nnmarkActionComplete(login);nmarkActionComplete(logout);nnmarkActionComplete(login);nn// [LOG] Reporting: loginn// [LOG] Reporting: logoutn

注意,即使我觸發了兩次 login 的 action,也不會有報告發生。完美,這就是我們需要的行為表現。

成功的兩點原因:

  1. login 標誌位已經被置為 true,所以值是沒有發生變化的
  2. when() 方法的副作用已經被銷毀,所以也不會再有追蹤發生

示例2:作為工作流的一部分來觸發操作

問題: 我們在一個工作流中包含了許多狀態。每一個狀態都是某些任務的映射,當工作流達到這個狀態時,這些任務會被執行。

解決辦法

從上面的描述中來看,唯一需要被觀察的是工作流中的狀態。每種狀態都有對應的任務需要執行,這些任務通過簡單的映射關係儲存。通過這些信息我們可以為我們的工作流建立模型:

class Workflow {nn constructor(taskMap) {n this.taskMap = taskMap;n this.state = observable({n previous: null,n next: nulln });nn this.transitionTo = action((name) => {n this.state.previous = this.state.next;n this.state.next = name;n });nn this.monitorWorkflow();n }nn monitorWorkflow() {n /* ... */n }n}nn// Usagenconst workflow = new Workflow({n start() {n console.log(Running START);n },nn process(){n console.log(Running PROCESS);n },nn approve() {n console.log(Running APPROVE);n },nn finalize(workflow) {n console.log(Running FINALIZE);nn setTimeout(()=>{n workflow.transitionTo(end);n }, 500);n },nn end() {n console.log(Running END);n }n});n

注意我們儲存了一個叫 state 的實例變數,它可以追蹤工作流現在和之前的狀態。我們同樣傳入了 state 到 task 的映射關係,儲存在 taskMap 中。

監聽工作流是這裡有趣的部分。在這個例子中,我們沒有像之前示例中有一次性的操作。一個工作流通常是在整個應用的生命周期中長期運轉的。這裡需要的是 autorun() 或者 reaction()。

狀態對應的任務只會在你過渡到這個狀態時觸發。所以在我們觸發任何副作用(任務)之前,我們需要等待 this.state.next的變化。等待一個變化意味著我們需要使用 reaction(),因為它只會在追蹤的被觀察變數發生變化時被觸發。所以我們的監聽函數代碼會像下面這樣:

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

reaction() 的第一個參數是追蹤函數,在這裡就是簡單的返回 this.state.next。當追蹤函數的返回值發生變化,就會觸發作用函數。作用函數接受當前的狀態,從 this.taskMap 中找出對應的任務並執行。

注意,我們也把工作流的實例傳入到任務中。這樣就可以把工作流過渡到其它狀態。

workflow.transitionTo(start);nnworkflow.transitionTo(finalize);nn// [LOG] Running STARTn// [LOG] Running FINALIZEn/* ... after 500ms ... */n// [LOG] Running ENDn

有趣的是,像 this.state.next 並且使用 reaction() 來觸發副作用的這種儲存簡單觀察變數的技術,還可以被用來做這些:

  • 通過 react-router 管理路由
  • 在演示的應用中導航
  • 基於一種模式切換不同的視圖

示例3:表單內容變化時顯示驗證信息

問題: 一堆文本框需要被驗證是一個經典了 Web 表單的使用場景。當它們驗證通過,你可以允許表單進行提交。

解決方法

讓我們給一個需要驗證表單欄位的簡單 FormData 類建立模型。

class FormData {nn constructor() {n extendObservable(this, {n firstName: ,n lastName: ,n email: ,n acceptTerms: false,nn errors: {},nn get valid() { // 這裡會變成 compute() 方法的屬性n return (this.errors === null);n }n });nn this.setupValidation(); // 我們會在下面看到n }nn}n

extendObservable() API是我們以前沒有見過的。通過應用它到我們類的實例(this)上,我們通過 ES5 的等價方式實現了 @observable 裝飾類的屬性。

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

接下來我們需要監聽所有欄位的變化,並且執行某些驗證邏輯。如果驗證邏輯執行通過,我們可以標記這個實體是可用的並且允許提交。可用性本身是通過一個計算屬性 valid 來被追蹤的。

既然驗證邏輯需要在 FromData 的整個生命周期中執行,我們將會使用 autorun() 方法。我們也可以使用 reaction() 方法,但是我們希望立即執行驗證而不是等待數據發生第一次變化。

class FormData {n setupValidation() {n autorun(() => {n // Dereferencing observables for trackingn const {firstName, lastName, email, acceptTerms} = this;n const props = {n firstName,n lastName,n email,n acceptTermsn };nn this.runValidation(props, {/* ... */})n .then(result => {n this.errors = result;n })n });n }nn runValidation(propertyMap, rules) {n return new Promise((resolve) => {n const {firstName, lastName, email, acceptTerms} = propertyMap;nn const isValid = (firstName !== && lastName !== && email !== && acceptTerms === true);n resolve(isValid ? null : {/* ... map of errors ... */});n });n }nn}n

在上面的代碼中,autorun() 方法會在被追蹤的觀察變數發生變化時自動觸發。注意為了讓 MobX 恰當的追蹤你的觀察變數,你需要使用間接引用(dereferencing)。

runValidation() 是同步觸發的,這就是我們為什麼返回一個 promise 對象。在上面的例子中這不重要,但是在實際場景中你可能會因為一些特殊的驗證給伺服器發送請求。當結果返回的時候我們會設置觀察變數 error 的值,它反過來也會更新計算屬性 valid。

如果你的驗證邏輯性能開銷很大,你甚至可以使用 autorunAsync(),它有一個參數可以對驗證邏輯的執行在短暫延遲後採取防抖措施。

現在讓我們的代碼跑起來。我們要通過 autorun() 創建一個簡單的控制台日誌器並且追蹤計算屬性 valid。

const instance = new FormData();nnautorun(() => {n // 追蹤這個變數,autorun() 可以在每一個文本框發生變化的時候執行n const validation = instance.errors;nn console.log(`Valid = ${instance.valid}`);n if (instance.valid) {n console.log(--- Form Submitted ---);n }nn});nn// 讓我們改變一些欄位ninstance.firstName = Pavan;ninstance.lastName = Podila;ninstance.email = pavan@pixelingene.com;ninstance.acceptTerms = true;n

輸出的日誌為:

Valid = falsenValid = falsenValid = falsenValid = falsenValid = falsenValid = truen--- Form Submitted ---n

因為 autorun() 會立即執行,你會看到最開始會有兩條額外的日誌,一條是因為 instance.errors,另一條是因為 instance.valid。剩下的四條是每次欄位發生變化時觸發的。

每個欄位的改變會觸發 runValidation() 方法,這個方法每次會在內部返回一個新的 error 對象。這會導致引用的 instance.errors 發生變化並且觸發 autorun() 方法來列印 valid 的值。最後,當我們設置了所有欄位的值, instance.errors 變成了 null(再次改變引用的值)並且列印出最終的 Valid = true。

所以簡單來說,我們通過讓表單欄位被觀察來進行表單驗證。同時添加一個額外的 errors 屬性和一個 valid 計算屬性來保證對可用性的追蹤。autorun 通過把所有事情綁在一起來控制它們。

示例4:追蹤是否所有已註冊的組件完成載入

問題: 有一系列已註冊的組件,我們想要在它們全部完成載入之前時保持追蹤。每一個組件都會暴露出一個 load() 方法,這個方法返回一個 promise 對象。如果這個 promise 對象進入 resolve 狀態,我們就把這個組件標記為已載入。如果 promise 進入 reject 狀態,我們把這個組件標記為載入失敗。當所有的組件完成載入,我們會報告整個系列的組件完成載入或失敗。

解決辦法

讓我們先來看一下我們面對的組件。我們創建一系列的組件,它們會隨機的報告它們的載入狀態。注意,有一些是非同步的。

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

第二部是為 Tracker 類設計觀察變數的狀態。組件的 load() 方法不會以一個特定的順序完成。所以我們需要一個可觀察的數組去保存每一個組件的 loaded 狀態。我們也要追蹤每一個組件的 reported 狀態。

當所有的組件已經發送報告(reported),我們可以發布所有組件最終的 loaded 狀態。下面的代碼創建了觀察變數。

class Tracker {nn constructor(components) {n this.components = components;nn extendObservable(this, {nn // 創建一個組件狀態的可觀察數組,n // 每個組件都有n states: components.map(({name}) => {n return {n name,n reported: false,n loaded: undefinedn };n }),nn // 所有組件完成報告時,獲得的計算屬性n get reported() {n return this.states.reduce((flag, state) => {n return flag && state.reported;n }, true);n },nn // 所有組件完成載入時,獲得的計算屬性n get loaded() {n return this.states.reduce((flag, state) => {n return flag && !!state.loaded;n }, true);n },nn // 一個標記 reported 和 loaded 的 action 方法n mark: action((name, loaded) => {n const state = this.states.find(state => state.name === name);nn state.reported = true;n state.loaded = loaded;n })nn });nn }n}n

我們再次使用了 extendObservable() 方法來設置我們可觀察的狀態。reported 和 loaded 計算屬性追蹤何時組件完成它們的載入。mark() 是我們用來改變觀察變數狀態的 action 方法。

順便說一下,無論在你需要從你的觀察變數中提取值的任何地方,使用計算屬性是最佳實踐。把計算屬性當做生產值的一種觀察變數。計算屬性值也會被緩存,可以表現的更好。另一方面 autorun 和 reaction 不會產出值,而是為創建副作用提供必要的一層包裹。

為了開始追蹤,我們在 Tracker類中新建一個 track() 屬性。這會觸發每個組件的 load() 方法並且等待返回 Promise 對象的結果。基於這些 track() 方法會標記每一個組件的載入狀態。

當所有組件都完成報告(reported),追蹤者會報告最終的 loaded 狀態。我們在這裡使用 when() 方法,因為我們要等待 this.reported 變成 true。這個副作用僅僅需要觸發一次,是個 when() 方法絕佳的適用場景。

下面的代碼實現了上面我們描述的過程:

class Tracker {nn /* ... */ nn track(done) {nn when(n () => this.reported,n () => {n done(this.loaded);n }n );nn this.components.forEach(({name, load}) => {n load()n .then(() => {n this.mark(name, true);n })n .catch(() => {n this.mark(name, false);n });n });n }nn setupLogger() {n autorun(() => {n const loaded = this.states.map(({name, loaded}) => {n return `${name}: ${loaded}`;n });nn console.log(loaded.join(,));n });n }n}n

setupLogger() 方法不是真正的真正的解決辦法,只是用來列印報告的。這是獲取我們的解決方案是否工作的一個好辦法。

現在我們要做如下的嘗試:

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

輸出的日誌顯示我們的方法按照我們的預期執行。當組件報告的時候,我們列印當前每個組件的 loaded 狀態。當所有的組件都完成報告時,this.reported 變為 true,我們就會看到 All Components Loaded的信息。

first: undefined, second: undefined, third: undefinednfirst: true, second: undefined, third: undefinednfirst: true, second: undefined, third: truenAll Components Loaded = falsenfirst: true, second: false, third: truen

思維轉變過來了嗎?

希望上邊一系列的示例讓你對 MobX 有了新的思考。

MobX 就是在一個可觀察的數據表中產生的副作用。

  1. 設計可觀察的狀態
  2. 創建 action 方法來改變可觀察的狀態
  3. 放入追蹤函數(when, autorun, reaction)去響應可觀察狀態的變化

上面這個步驟可以適用於更複雜的場景,比如你需要在某些事情發生變化後追蹤某些事情,只需要重複進行步驟1-3即可。


推薦閱讀:

React 許可證雖嚴苛,但不必過度 react
React全家桶實現一個簡易備忘錄
The Redux Journey 翻譯及分析(上)
React異常處理
Learn CSS Animations: From A Designer's Perspective

TAG:MobX | React | 前端开发 |