原文連接:github.com/whinc/blog/…javascript
導讀:MobX 是一個優秀的響應式狀態管理庫,在流行的狀態管理庫 Redux 以外爲咱們提供了其餘選擇。若是你尚未嘗試過 MobX,我強烈建議你繼續閱讀本文,並跟着示例動手實踐體驗一下。本文是 MobX 的入門教程,文章包含 MobX 的設計哲學、狀態派生模型、核心API以及與 React 的集成。html
MobX 是一個簡單、可伸縮的響應式狀態管理庫。經過 MobX 你能夠用最直觀的方式修改狀態,其餘的一切 MobX 都會爲你處理好(如自動更新UI),而且具備很是高的性能。java
MobX 文檔的概念和 API 比較多,初次接觸時可能會感受無從下手或者抓不住重點,本文嘗試提供一份 MobX 核心的知識的簡明教程,做爲閱讀官方文檔以前的熱身。react
本文不包含任何跟裝飾器(decorator)相關的內容,由於裝飾器並非 MobX 的核心內容,它只是語法糖,沒有它照樣可使用 MobX。git
目錄github
在學習使用 MobX API 以前,咱們首先要了解 MobX 的設計哲學,它是咱們在思考 MobX 應用時的心智模型,能夠幫助咱們更好的使用 MobX API。typescript
MobX 的設計哲學歸納起來就一句話:Anything that can be derived from the application state, should be derived. Automatically. (任何能夠從應用狀態中派生的內容,都應當自動地被派生。)api
理解這句話的關鍵是搞清楚【派生】的含義是什麼?在 MobX 中【派生】的含義比較普遍,包括:數組
咱們平時常看到的狀態響應模型,其中的響應就能夠看作是狀態的一種派生,MobX 將這種模型進行泛化,造成更通用的狀態派生模型,接下來會詳細介紹。緩存
狀態響應模型歸納起來主要包含三個要素:定義狀態、響應狀態、修改狀態(以下圖所示)。
MobX 中經過observable
來定義可觀察狀態, 它接受任意 JS 值(包括Object
、Array
、Map
、Set
),返回原始數據的代理對象,代理對象與原始數據具備相同的接口,你能夠把代理對象當作原始數據使用。
// 定義狀態
const store = observable({
count: 0
});
複製代碼
MobX 中經過autorun
來定義狀態變化時要執行的響應操做,它接受一個函數。此後,每當observable
中定義的狀態發生變化時,MobX 都會當即執行該函數。
// 響應狀態
autorun(() => {
console.log("count:", store.count);
});
複製代碼
MobX 中修改狀態和修改原始數據的方式沒什麼區別,這也是 MobX 的優勢——符合直覺的操做方式。
// 修改狀態
store.count += 1;
複製代碼
把上面的部分串起來就是一個最簡單的 MobX 示例了(以下),示例中每次修改 count 的值時會自動打印一條日誌,而且日誌包含最新的 count 值。這個示例揭示了 MobX 中最核心的功能。
import { observable, autorun } from "mobx";
// 1. 定義狀態
const store = observable({
count: 0
});
// 2. 響應狀態
autorun(() => {
console.log("count:", store.count);
});
// count: 0
// 3. 修改狀態
store.count += 1;
// count: 1
複製代碼
上面例子中,首先經過 MobX 提供的observable
函數定義狀態,例子的狀態是{count: 0}
。而後經過經過 MobX 提供的autorun
函數定義狀態響應函數,例子中是一條打印當前count
值得的語句,當定義的狀態中的任何值發生變化時,該響應函數會當即執行,而且是由 MobX 自動完成的。最後是修改狀態,和操做對象屬性同樣,例子中是對count
屬性進行自增操做。
從上一節中咱們知道 MobX 的狀態響應函數是在狀態變化時執行某些操做。實際應用中這些操做可分爲兩類:帶反作用(如打印日誌、渲染 UI、請求網絡等)和不帶反作用(如計算數組的長度)。MobX 將這些操做統稱爲派生(Derivations),便可從應用狀態中派生出來的任何內容。
其中不帶反作用的操做是一個純函數,通常是根據當前狀態計算返回一個新的值。爲了區分帶反作用和不帶反作用的兩類狀況,MobX 將 Derivations 概念細分紅兩個概念 Reactions 和 Computed values 來區分兩者。此外,MobX 還提供了一個可選的概念 Action 來表示對 State 的修改操做,用於約束和預測應用狀態的修改行爲。這些概念彙總在一塊兒以下圖:
下面是一個簡單的示例,完整展現了上圖中涉及的全部概念。
index.html 文件
<html>
<body>
<div id="container"></div>
</body>
</html>
複製代碼
index.js 文件
import { observable, autorun, computed, action } from "mobx"
const containerEl = document.querySelector("#container");
// State
const store = observable({
count: 0
});
// Actions
window.increaseCount = action(() => store.count++);
// Computed values
const doubleCount = computed(() => 2 * store.count);
// Reactions
autorun(() => {
containerEl.innerHTML = ` <span>${store.count} * 2 = ${doubleCount.get()}</span> <button onclick='window.increaseCount()'>+1</button> `;
});
// 0 * 2 = 0
// (點擊按鈕)
// 1 * 2 = 2
// (點擊按鈕)
// 2 * 2 = 4
複製代碼
這個示例展現了一個簡單的乘法,點擊按鈕後數據會自增,同時計算的結果也隨之更新。相比上一個例子,它有幾個值得注意的區別:
computed
定義計算值,並返回計算值對象doubleCount
,經過其get/set
方法訪問內部計算值。computed
接受一個返回計算值的函數,在函數內部可使用observable
定義的狀態數據(如count
),每當count
的值發生變化時,doubleCount
內部的計算值會自動更新。action
定義狀態修改操做,action
接受一個函數,並返回一個簽名相同的函數。在函數內部可直接修改obsevable
定義的狀態(如count
)觸發autorun
和computed
從新運行。autorun
中的響應操做替換成了渲染 UI,每當狀態發生變化時從新渲染 UI。MobX 的 API 可分爲四類:定義狀態(observable
)、響應狀態(autorun
, computed
)、修改狀態(action
)、輔助函數。下面挑出最核心的 API 進行重點介紹,但不會涉及 API 的詳細用法(請參考MobX Api Reference)。
MobX 目前同時支持 v4 和 v5 兩個版本,這兩個版本的 API 是相同的(功能相同),他們的區別在於內部實現數據響應的方式不一樣,若是使用 v4 版本時須要注意文檔中標註的注意狀況。
observable
observable
用於定義可觀察狀態,其類型定義以下(已簡化):
<T extends Object>(value: T): T & IObservableObject;
<T = any>(value: T[]): IObservableArray<T>;
<K = any, V = any>(value: Map<K, V>): ObservableMap<K, V>;
<T = any>(value: Set<T>): ObservableSet<T>;
複製代碼
它接受Object/Array/Map/Set
類型的數據做爲參數,並返回對應數據的代理對象,代理對象和原數據類型具備相同的接口,你能夠像使用原始數據同樣使用代理對象。例如:
import { observable } from "mobx";
const object = observable({a: 1})
console.log(object.a)
const array = observable([1, 2])
console.log(array[0])
const map = observable(new Map({a: 1}))
console.log(map.get('a'))
const set = observable(new Set([1, 2]))
console.log(set.has(1))
複製代碼
observable
觀察的數據能夠嵌套,嵌套的數據也會被觀察。例如:
import { observable, autorun } from "mobx";
const store = observable({
a: {
b: [1, 2]
}
})
autorun(() => console.log(store.a.b[0]))
// 1
store.a.b[0] += 1
// 2
複製代碼
observable
支持動態添加可觀察狀態。例如:
import { observable, autorun } from "mobx";
const store = observable({});
autorun(() => {
console.log("a =", store.a);
});
// a = undefined
store.a = 1;
// a = 1
複製代碼
動態添加可觀察狀態,僅適用於 MobX v5+ 版本,MobX v4 及如下版本須要藉助輔助函數,詳見Direct Observable manipulation。
autorun
autorun
用於定義響應函數,其類型定義以下(已簡化):
autorun(reaction: () => any): IReactionDisposer;
複製代碼
autorun
接受一個響應函數 reaction,並在定義時當即執行一次 reaction 函數, reaction 函數內部能夠執行帶有反作用的操做。 之後,每當依賴狀態發生變化時,autorun
自動從新運行 reaction 函數。autorun
第一次運行 reaction 函數是爲了蒐集依賴狀態——運行 reaction 過程當中實際使用的狀態(經過obj.name
或obj['name']
解引用方式使用的狀態)。
例以下面例子,autorun
中使用了狀態a
,所以當狀態a
的值發生變化時,會執行響應函數。而狀態b
雖然被列爲可觀察狀態,但因爲在autorun
中沒有被實際使用,所以當狀態b
的值發生變化時,不會執行 響應函數 。這是 MobX 的「聰明」之處,它能根據狀態的實際使用狀況,細粒度地控制更新範圍。因爲減小了沒必要要的執行開銷,從而提高了程序性能。
import { observable, autorun } from "mobx";
const store = observable({
a: 1,
b: 2
});
autorun(() => {
console.log("a =", store.a);
});
// a = 1
store.a += 1;
// a = 2
store.b += 1;
// (無輸出)
複製代碼
讓咱們回顧一下 MobX 的使用,經過observable
定義狀態,在autorun
中使用狀態,當autorun
中使用到的狀態發生變化時,該autorun
從新執行。絕大部分狀況下它都工做的很好,若是你遇到修改了狀態而autorun
沒有如你預期的那樣運行,這時候你須要深刻了解 MobX 是如何響應狀態變化的,推薦閱讀What does MobX react to?。
computed
computed
用於定義計算值,其類型定義以下(已簡化):
<T>(func: () => T) => { get(): T, set(value: T): void}
複製代碼
computed
與autorun
類似,他們都會在依賴的狀態發生變化時會從新運行,不一樣之處是computed
接收的是純函數而且返回一個計算值,這個計算值在狀態變化時會自動更新,計算值能夠在autorun
中使用。
例以下面例子中,ca
是一個計算值,它依賴狀態a
的值,當狀態a
的值發生變化時,ca
會從新計算值。計算值ca
是一個「裝箱」對象,須要經過get/set
訪問內部值,只有這樣才能保持計算值的引用不變而內部值又是可變的。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
return 10 * store.a;
});
autorun(() => {
console.log(`${store.a} * 10 = ${ca.get()}`);
});
// 1 * 10 = 10
store.a += 1;
// 2 * 10 = 20
store.a += 1;
// 3 * 10 = 30
複製代碼
因爲computed
被視做是純函數,MobX 提供了許多開箱即用的優化措施,例如對計算值的緩存和惰性計算。
computed
值會被緩存
每當讀取computed
值時,若是其依賴的狀態或其餘computed
值未發生變化,則使用上次的緩存結果,以減小計算開銷,對於複雜的computed
值,緩存能夠大大提升性能。
例以下面例子中,computed
值ca
和cb
分別依賴狀態a
和b
,第一次執行autorun
時,ca
和cb
都會從新計算,而後修改狀態a
的值,第二次執行autorun
時,只有ca
會從新計算,而cb
則使用上次的緩存結果。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1,
b: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
const cb = computed(() => {
console.log("recomputed cb");
return 10 * store.b;
});
autorun(() => {
console.log(
`a = ${store.a}, ca = ${ca.get()}, b = ${store.b}, cb = ${cb.get()}`
);
});
// recomputed ca
// recomputed cb
// a = 1, ca = 10, b = 1, cb = 10
store.a += 1;
// recomputed ca
// a = 2, ca = 20, b = 1, cb = 10
複製代碼
爲了觀察
computed
的計算過程,插入了打印日誌的語句,這會帶有反作用,實際中不要這樣作
computed
值會惰性計算
只有computed
值被使用時才從新計算值。反言之,即便computed
值依賴的狀態發生了變化,可是它暫時沒有被使用,那麼它不會從新計算。
例以下面例子中,computed
值ca
依賴狀態a
。當狀態a
的值小於 3 時,autorun
運行時只打印狀態a
的值,因爲computed
值ca
未被使用,因此ca
不會重新計算。當狀態a
的值增加到 3 之後,autorun
運行時同時打印狀態a
和computed
值ca
,因爲computed
值ca
被使用了,因此ca
會從新計算。
import { observable, autorun, computed } from "mobx";
const store = observable({
a: 1
});
const ca = computed(() => {
console.log("recomputed ca");
return 10 * store.a;
});
autorun(() => {
if (store.a >= 3) {
console.log(`a = ${store.a}, ca = ${ca.get()}`);
} else {
console.log(`a = ${store.a}`);
}
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// recomputed ca
// a = 3, ca = 30
複製代碼
action
action
用於定義狀態修改操做,其類型定義以下(已簡化):
<T extends Function>(fn: T) => T
複製代碼
雖然沒有action
也能夠直接修改狀態,可是經過action
顯式地修改狀態,使得狀態的變化可預測(狀態的變化能定位到是哪一個action
引發的)。此外,action
函數是事務型的,經過action
修改狀態時,響應函數不會當即執行,而是等到action
結束後才執行,這有助於提高性能。
例以下面例子中,展現了修改狀態的兩種方式,一種是直接修改狀態a
,另外一種是經過調用預先定義的action
修改狀態。值得注意的是,在一次action
中連續兩次修改狀態a
的值,只會觸發一次autorun
的執行。
import { observable, autorun, action } from "mobx";
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
store.a += 1;
// a = 2
store.a += 1;
// a = 3
const increaseA = action(() => {
store.a += 1;
store.a += 1;
});
increaseA();
// a = 5
複製代碼
對 MobX 進行一些配置後,可使action
成爲修改狀態的惟一方式,這能夠避免不受約束的修改狀態行爲發生,有利於提高項目的可維護性。
例以下面例子中,配置 enforceActions 爲"always"
後,就只能經過action
修改狀態了,若是嘗試直接修改狀態將會觸發異常。
import { observable, autorun, action, configure } from "mobx";
// 強制只能經過 action 修改狀態
configure({
enforceActions: "always"
});
const store = observable({
a: 1
});
autorun(() => {
console.log(`a = ${store.a}`);
});
// a = 1
// store.a += 1;
// 直接修改狀態,將會拋出以下異常
// Error: [mobx] Since strict-mode is enabled, changing observed
// observable values outside actions is not allowed.
// Please wrap the code in an `action` if this change is intended.
const increaseA = action(() => {
store.a += 1;
});
increaseA();
// a = 2
複製代碼
MobX 是框架無關的,你能夠單獨使用它,也能夠與任何流行的 UI 框架進行一塊兒使用,MobX 官方提供了 React/Vue/Angular 等流行框架的綁定實現。MobX 最多見的是與 React 一塊兒使用,他們的綁定實現是 mobx-react(或 mobx-react-lite)。
mobx-react 提供了一個observer
方法, 它是一個高階組件,它接收 React 組件並返回一個新的 React 組件,返回的新組件能響應(經過observable
定義的)狀態的變化,即組件能在可觀察狀態變化時自動更新。observer
方法是對 MobX 提供的autorun
方法和 React 組件更新機制的封裝,以便於在 React 中使用,你依然能夠在 React 中使用autorun
來更新組件。下面是observer
方法的類型聲明,它支持組件類和函數組件。
function observer<T extends React.ComponentClass | React.FunctionComponent>(target: T): T 複製代碼
下面是組件類使用 MobX 的示例,經過observable
定義可觀察狀態,並經過observer
包裹組件,以後組件事件處理方法中修改狀態後,組件會自動更新,無需手動調用 React 的setState()
來更新組件。
import React from "react";
import { observable } from "mobx";
import { observer } from "mobx-react";
class Counter extends React.Component {
constructor(props) {
super(props);
this.store = observable({
count: 0
});
}
render() {
return (
<button onClick={() => this.store.count++}> {this.store.count} </button>
)
}
}
export default observer(Counter);
複製代碼
下面是函數組件使用 MobX 的示例,與上面類組件相似,區別是使用 mobx-react 提供的useLocalStore
定義客觀察狀態,useLocalStore
內部也是使用observable
定義可觀察狀態。
import React, { useMemo } from "react";
import { observable } from "mobx";
import { observer, useLocalStore } from "mobx-react";
const Counter = () => {
const store = useLocalStore(() => ({
count: 0
}));
// 等價於下面
// const store = useMemo(() => observable({ count: 0 }), []);
return (
<button onClick={() => store.count++}> {store.count} </button>
)
};
export default observer(Counter);
複製代碼
MobX 的設計哲學是「可從應用狀態中派生的任何內容都應當自動的被派生」,後半句有兩個關鍵字:自動和派生。心中秉持這一設計哲學,再來看 MobX 的派生狀態模型就比較清晰了,Computed values 和 Reactions 均可以視做是從 State 中派生出的,State 變化時觸發 Computed values 的從新計算和 Reations 的從新運行。爲了讓派生能自動的進行,MobX 經過Object.definePropery
或Proxy
方式攔截對象的讀寫操做,從而容許用戶以天然的方式來修改狀態,MobX 負責更新派生的內容。
MobX 提供了幾個核心 API 來幫助定義狀態(observable
)、響應狀態(autorun
, computed
)和修改狀態(action
),經過這些 API 可讓程序當即具有響應式能力。這些 API 接口並不複雜,但要熟練使用,須要深刻理解 MobX 響應機制,文中經過一些簡單的示例來輔助理解這些 API 的行爲。
MobX 能夠單獨使用,也能夠與任何流行的 UI 框架一塊兒使用,Github 上能夠找到 MobX 與流行框架的綁定實現。不過 MobX 最多見的是與 React 一塊兒使用,mobx-react 是流行的 MobX 和 React 的綁定實現庫,本文介紹了它在組件類和函數組件上的一些基本用法。