[MobX State Tree資料元件化開發][1]:MST基礎

awaw00發表於2019-01-25

?系列文章目錄?

預備知識

在正式進入主題前,你需要確認一下,是否已經掌握下面幾個工具和庫的使用:

  • MobX:這是MST的核心,MST中儲存的響應式“狀態”都是MobXObservable
  • React:使用React來測試MST的功能非常簡單
  • TypeScript:後文中會使用TS來編寫示例程式碼,TS強大的智慧提示和型別檢查,有助於快速掌握MST的API

上面列舉的工具和庫都有非常豐富的文件和教程,不太熟悉的同學最好先自學一下。

安裝

MST依賴MobX。

專案中執行yarn add mobx mobx-state-tree即可完成安裝。

MobX有兩個版本,新版本需要瀏覽器Proxy支援,一些老舊的瀏覽器並不支援,需要相容老瀏覽器的請安裝mobx@4:yarn add mobx@4 mobx-state-tree

Type、Model

使用MST來維護狀態,首先需要讓MST知道,這個狀態的結構是什麼樣的。

MST內建了一個型別機制。通過型別的組合就可以定義出整個狀態的形狀。

並且,在開發環境下,MST可以通過這個定義好的形狀,來判斷狀態的值和形狀與其對應的型別是否匹配,確保狀態的型別與預期一致,這有助於在開發時及時發現資料型別的問題:

MST型別檢查

MST提供的一個重要物件就是types,在這個物件中,包含了基礎的元型別(primitives types),如stringbooleannumber,還包含了一些複雜型別的工廠方法和工具方法,常用的有modelarraymapoptional等。

model是一個types中最重要的一個type,使用types.model方法得到的就是Model,在Model中,可以包含多個type或者其他Model

一個Model可以看作是一個節點(Node),節點之間相互組合,就構造出了整棵狀態樹(State Tree)。

MST可用的型別和型別方法非常多,這裡不一一列舉,可以在這裡檢視完整的列表。

完成Model的定義後,可以使用Model.create方法獲得Model的例項。Model.create可以傳入兩個引數,第一個是Model的初始狀態值,第二個引數是可選引數,表示需要給Model及子Model的env物件(環境配置物件),env用於實現簡單的依賴注入功能,在後續文章中再詳細說明。

Props

props指的是Model中的屬性定義。props定義了這個Model維護的狀態物件包含哪些欄位,各欄位對應的又是什麼型別。

拿開篇中的“商品”作為例子:

import { types } from 'mobx-state-tree';

export const ProductItem = types.model('ProductItem', {
    prodName: types.string,
    price: types.number,
});
複製程式碼

types.model方法的第一個引數為Model設定了名稱,第二個引數傳入了一個物件,這個物件就是Model的props。

上面程式碼中,指定了ProductItem這個Model包含了型別為stringprodName屬性和型別為numberprice屬性。

注意,可以省略types.model的第二個引數,然後使用model.props方法來定義props。

export const ProductItem = types
    .model('ProductItem')
    .props({
        prodName: types.string,
        price: types.number,
    });
複製程式碼

上面的兩份程式碼得到的ProductItem是相同的(實際上有一些細微差別,但可以完全忽略)。

定義了props之後,在Model的例項上可以訪問到相應的欄位:

const productItem = ProductItem.create({prodName: '商品標題xxx', price: 99.9});

console.log(productItem.prodName); // 商品標題xxx
console.log(productItem.price); // 99.9
複製程式碼

Views

views是Model中一系列衍生資料獲取衍生資料的方法的集合,類似Vue元件的computed計算屬性。

在定義Model時,可以使用model.views方法定義views。

export const ProductItem = types
    .model('ProductItem', {
        prodName: types.string,
        price: types.number,
        discount: types.number,
    })
    .views(self => ({
        get priceAfterDiscount () {
            return self.price - self.discount;
        }
    }));
複製程式碼

上面程式碼中,定義了priceAfterDiscount,表示商品的折後價格。呼叫.views方法時,傳入的是一個方法,方法的引數self是當前Model的例項,方法需要返回一個物件,表示Model的views集合。

需要注意的是,定義views時有兩種選擇,使用getter或者不使用。使用getter時,衍生資料的值會被快取直到依賴的資料傳送變化。而不使用時,需要通過方法呼叫的方式獲取衍生資料,無法對計算結果進行快取。儘可能使用getter,有助於提升應用的效能。

Actions

actions是用於更新狀態的方法集合。

在建立Model時,使用model.actions方法來定義actions:

const Root = types
    .model('Root', {
        str: types.string,
    })
    .actions(self => ({
        setStr (val: string) {
            self.str = val;
        }
    }));
    
const root = Root.create({str: 'mobx'});
root.setStr('mst');
複製程式碼

在安全模式下,所有對狀態的更新操作必須在actions中執行,否則會報錯:

actions外部更新狀態報錯

可以使用unprotect方法解除安全模式(不推薦):

import { types, unprotect } from 'mobx-state-tree';

const Root = types.model(...);
unprotect(Root);

root.str = 'mst'; // ok
複製程式碼

除了通常意義上用來更新狀態的actions外,在model.actions方法中,還可以設定一些特殊的actions:

  • afterCreate
  • afterAttach
  • beforeDetach
  • beforeDestroy

從名字上可以看出來,上面四位都是生命週期方法,可以使用他們在Model的各個生命週期執行一些操作:

const Model = types
    .model(...)
    .actions(self => ({
        afterCreate () {
            // 執行一些初始化操作
        }
    }));
複製程式碼

具體的MST生命週期在後續文章中再詳細討論。

非同步Action、Flow

非同步更新狀態是非常常見的需求,MST從底層支援非同步action。

const model = types
    .model(...)
    .actions(self => ({
        // async/await
        async getData () {
            try {
                const data = await api.getData();
                ...
            } catch (err) {
                ...
            }
            ...
        },
        // promise
        updateData () {
            return api.updateData()
                .then(...)
                .catch(...);
        }
    }));
複製程式碼

需要注意,上文提到過:

在安全模式下,所有對狀態的更新操作必須在actions中執行,否則會報錯

若使用Promise、async/await來編寫非同步Action,在非同步操作之後更新狀態時,程式碼執行的上下文會脫離action,導致狀態在action之外被更新而報錯。這裡有兩種解決辦法:

  1. 將更新狀態的操作單獨封裝成action
  2. 編寫一個runInAction的action在非同步操作中使用
// 方法1
const Model = types
    .model(...)
    .actions(self => ({
        setLoading (loading: boolean) {
            self.loading = loading;
        },
        setData (data: any) {
            self.data = data;
        },
        async getData () {
            ...
            self.setLoading(true); // 這裡因為在非同步操作之前,直接賦值self.loading = true也ok
            const data = await api.getData();
            self.setData(data);
            self.setLoading(false);
            ...
        }
    }));
    
// 方法2
const Model = types
    .model(...)
    .actions(self => ({
        runInAction (fn: () => any) {
            fn();
        },
        async getData () {
            ...
            self.runInAction(() => self.loading = true);
            const data = await api.getData();
            self.runInAction(() => {
                self.data = data;
                self.loading = false;
            });
            ...
        }
    }));
複製程式碼

方法1需要額外封裝N個action,比較麻煩。方法2封裝一次就可以多次使用。

但是在某些情況下,兩種方法都不夠完美:一個非同步action被分割成了N個action呼叫,無法使用MST的外掛機制實現整個非同步action的原子操作、撤銷/重做等高階功能。

為了解決這個問題,MST提供了flow方法來建立非同步action:

import { types, flow } from 'mobx-state-tree';

const model = types
    .model(...)
    .actions(self => {
        const getData = flow(function * () {
            self.loading = true;
            try {
                const data = yield api.getData();
                self.data = data;
            } catch (err) {
                ...
            }
            self.loading = false;
        });
        
        return {
            getData
        };
    })
複製程式碼

使用flow方法需要傳入一個generator function,在這個生成器方法中,使用yield關鍵字可以resolve非同步操作。並且,在方法中可以直接給狀態賦值,寫起來更簡單自然。

Snapshot

snapshot即“快照”,表示某一時刻,Model的狀態序列化之後的值。這個值是標準的JS物件。

使用getSnapshot方法獲取快照:

import { getSnapshot } from 'mobx-state-tree';

cosnt Model = types.model(...);
const model = Model.create(...);

console.log(getSnapshot(model));
複製程式碼

使用applySnapshot方法可以更新Model的狀態:

import { applySnapshot } from 'mobx-state-tree';

...
applySnapshot(model, {
    msg: 'hello'
});
複製程式碼

通過applySnapshot方法更新狀態時,傳入的狀態值必須匹配Model的型別定義,否則會報錯:

[MobX State Tree資料元件化開發][1]:MST基礎

getSnapshotapplySnapshot方法都可以用在Model的子Model上使用。

Volatile State

在MST中,props對應的狀態都是可持久化的,也就是可以序列化為標準的JSON資料。並且,props對應的狀態必須與props的型別相匹配。

如果需要在Model中儲存無需持久化,並且資料結構或型別無法預知的動態資料,可以設定為Volatile State

Volatile State使用model.volatile方法定義:

import { types } from 'mobx-state-tree';
import { autorun } from 'mobx';

const Model = types
    .model('Model')
    .volatile(self => ({
        anyData: {} as any
    }))
    .actions(self => ({
      runInAction (fn: () => any) {
        fn();
      }
    }));
    
const model = Model.create();

autorun(() => console.log(model.anyData));

model.runInAction(() => {
  model.anyData = {a: 1};
});

model.runInAction(() => {
  model.anyData.a = 2;
});
複製程式碼

和actions及views一樣,model.volatile方法也要傳入一個引數為Model例項的方法,並返回一個物件。

執行上面程式碼,可得到如下輸出:

[MobX State Tree資料元件化開發][1]:MST基礎

程式碼中使用Mobx的autorun方法監聽並列印model.anyData的值,圖中一共看到2次輸出:

  1. anyData的初始值
  2. 第一次更新anyData後的值

但是第二次為anyData.a賦值並沒有執行autorun。

由此可見,Volatile State的值也是Observable,但是隻會響應引用的變化,是一個非Deep Observable

volatile demo程式碼

可以點開上面的連結,修改其中的程式碼,熟悉一下上面提到的幾個方法的使用。

小結

本章介紹了MST的基礎概念和重要的幾個API,後面會給大家講解使用MST搭配React來實現一個完整的Todo Listdemo。

喜歡本文歡迎關注和收藏,轉載請註明出處,謝謝支援。

相關文章