MobX 上手指南

語言: CN / TW / HK

之前用 Redux 比較多,一直聽說 Mobx 能讓你體驗到在 React 裡面寫 Vue 的感覺,今天打算嘗試下 Mobx 是不是真的有寫 Vue 的感覺。

題外話

在介紹 MobX 的用法之前,先說點題外話,我們可以看一下 MobX 的中文簡介。在 MobX 的中文網站上寫著:

MobX 是一個經過戰火洗禮的庫,它通過透明的函式響應式程式設計使得狀態管理變得簡單和可擴充套件。

資料流

“戰火洗禮的庫” 怎麼看都感覺很奇怪,讀起來很拗口😂,而且網上很多介紹 MobX 的文章都是這麼寫的,在 github 翻閱其 README 發現寫的是:

MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP).

可以看到作者原本要表達的意思是 MobX 是經過了許多的測試,擁有比較強的健壯性。下面是通過谷歌翻譯的結果,看起來也比中文網的表達要準確一些。

谷歌翻譯

雖然,我的英文水平也很菜,還是會盡量看官方的文件,這樣可以避免一些不必要的誤解。

如何使用?

言歸正傳,MobX 現在的最新版是 6.0,這個版本的 API 相比於之前有了極大的簡化,可以說更加好用了。之前的版本是裝飾器風格的語法糖,但是裝飾器在現在的 ES 規範中並不成熟,而且引入裝飾器語法也在增加打包後的程式碼體積。綜合考慮後,MobX 6.0 取消了裝飾器語法的 API。

響應式物件

MobX 通過 makeObservable 方法來構造響應式物件,傳入的物件屬性會通過 Proxy 代理,與 Vue 類似,在 6.0 版本之前使用的是 Object.defineProperty API,當然 6.0 也提供了降級方案。

import { configure, makeObservable, observable, action, computed } from 'mobx'

// 使用該配置,可以將 Proxy 降級為 Object.defineProperty
configure({ useProxies: "never" });

// 構造響應物件
const store = makeObservable(
  // 需要代理的響應物件
  {
    count: 0,
    get double() {
      return this.count * 2
    },
    increment() {
      this.count += 1
    },
    decrement() {
      this.count -= 1
    }
  },
  // 對各個屬性進行包裝,用於標記該屬性的作用
  {
    count: observable, // 需要跟蹤的響應屬性
    double: computed,  // 計算屬性
    increment: action, // action 呼叫後,會修改響應物件
    decrement: action, // action 呼叫後,會修改響應物件
  }
)
複製程式碼

我們在看看之前版本的 MobX,使用裝飾器的寫法:

class Store {
  @observable count = 0
  constructor() {
    makeObservable(this)
  }
  @action increment() {
    this.count++;
  }
  @action decrement() {
    this.count--;
  }
  @computed get double() {
    return this.count * 2
  }
}

const store = new Store()
複製程式碼

這麼看起來,好像寫法並沒有得到什麼簡化,好像比寫裝飾器還要複雜點。下面我們看看 6.0 版本一個更強大的 API:makeAutoObservable

makeAutoObservable 是一個更強大的 makeObservable,可以自動為屬性加上物件的包裝函式,上手成本直線下降。

import { makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  get double() {
    return this.count * 2
  },
  increment() {
    this.count += 1
  },
  decrement() {
    this.count -= 1
  }
})
複製程式碼

計算屬性

MobX 的屬性與 Vue 的 computed 一樣,在 makeAutoObservable 中就是一個 gettergetter 依賴的值一旦發生變化,getter 本身的返回值也會跟隨變化。

import { makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  get double() {
    return this.count * 2
  }
})
複製程式碼

store.count 為 1 時,呼叫 store.double 會返回 2。

修改行為

當我們需要修改 store 上的響應屬性時,我們可以通過直接重新賦值的方式修改,但是這樣會得到 MobX 的警告⚠️。

const store = makeAutoObservable({
  count: 0
});

document.getElementById("increment").onclick = function () {
  store.count += 1
}
複製程式碼

warn

MobX 會提示,在修改響應式物件的屬性時,需要通過 action 的方式修改。雖然直接修改也能生效,但是這樣會讓 MobX 狀態的管理比較混亂,而且將狀態修改放到 action 中,能夠讓 MobX 在內部的事務流程中進行修改,以免拿到的某個屬性還處於中間態,最後計算的結果不夠準確。

makeAutoObservable 中的所有方法都會被處理成 action。

import { makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  get double() {
    return this.count * 2
  },
  increment() { // action
    this.count += 1
  },
  decrement() { // action
    this.count -= 1
  }
})
複製程式碼

不同於 Vuex,將狀態的修改劃分為 mutation 和 action,同步修改放到 mutation 中,非同步的操作放到 action 中。在 MobX 中,不管是同步還是非同步操作,都可以放到 action 中,只是非同步操作在修改屬性時,需要將賦值操作放到 runInAction 中。

import { runInAction, makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  async initCount() {
    // 模擬獲取遠端的資料
    const count = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(10)
      }, 500)
    })
    // 獲取資料後,將賦值操作放到 runInAction 中
    runInAction(() => {
      this.count = count
    })
  }
})

store.initCount()
複製程式碼

如果不呼叫 runInAction ,則可以直接呼叫本身已經存在的 action。

import { runInAction, makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  setCount(count) {
    this.count = count
  },
  async initCount() {
    // 模擬獲取遠端的資料
    const count = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(10)
      }, 500)
    })
    // 獲取資料後,呼叫已有的 action
    this.setCount(count)
  }
})

store.initCount()
複製程式碼

監聽物件變更

無論是在 React 還是在小程式中想要引入 MobX,都需要在物件變更的時候,通知呼叫原生的 setState/setData 方法,將狀態同步到檢視上。

通過 autorun 方法可以實現這個能力,我們可以把 autorun 理解為 React Hooks 中的 useEffect。每當 store 的響應屬性發生修改時,傳入 autorun 的方法(effect)就會被呼叫一次。

import { autorun, makeAutoObservable } from 'mobx'

const store = makeAutoObservable({
  count: 0,
  setCount(count) {
    this.count = count
  },
  increment() {
    this.count++
  },
  decrement() {
    this.count--
  }
})

document.getElementById("increment").onclick = function () {
  store.count++
}

const $count = document.getElementById("count")
$count.innerText = `${store.count}`
autorun(() => {
  $count.innerText = `${store.count}`
})
複製程式碼

每當 button#increment 按鈕被點選的時候,span#count 內的值就會自動進行同步。👉檢視完整程式碼

效果演示

除了 autorun ,MobX 還提供了更精細化的監聽方法:reactionwhen

const store = makeAutoObservable({
  count: 0,
  setCount(count) {
    this.count = count
  },
  increment() {
    this.count++
  },
  decrement() {
    this.count--
  }
})

// store 發生修改立即呼叫 effect
autorun(() => {
  $count.innerText = `${store.count}`
});

// 第一個方法的返回值修改後才會呼叫後面的 effect
reaction(
  // 表示 store.count 修改後才會呼叫
  () => store.count,
  // 第一個引數為當前值,第二個引數為修改前的值
  // 有點類似與 Vue 中的 watch
  (value, prevValue) => {
    console.log('diff', value - prevValue)
  }
);

// 第一個方法的返回值為真,立即呼叫後面的 effect
when(() => store.count > 10, () => {
  console.log(store.count)
})
// when 方法還能返回一個 promise
(async function() {
  await when(() => store.count > 10)
  console.log('store.count > 10')
})()
複製程式碼

總結

MobX 的介紹到這裡就結束了,本文只是大致的列舉了一下 MobX 的 API,希望大家能有所收穫。後續打算再深入研究下 MobX 的實現,等我研究好了,再寫篇文章來分享。