We-Mobx 從小程序狀態(tài)管理到如何優(yōu)雅地實現(xiàn)一個工具庫

起因

在使用現(xiàn)成的小程序狀態(tài)管理方案時踩了個小坑箫锤,遂考慮能否在小程序引入前端生態(tài)中較為成熟的狀態(tài)管理方案搁痛。出于易用性和可拓展性的考量,這里選擇使用 mobx逼侦。

MobX 是一個簡單匿辩、可擴展的狀態(tài)管理方案腰耙。關(guān)于 MobX 的介紹與使用請移步 MobX

如果只是想在小程序中使用 mobx,可以直接參考 文檔 食用铲球。

過程

本次設(shè)計與實現(xiàn)除了完成基本的 mobx 與小程序之間的綁定功能挺庞,還希望能遵循幾個要點(痛點):

  • 簡潔、靈活睬辐、完善的 API
  • 函數(shù)式的實現(xiàn)與調(diào)用
  • 完善的類型支持

?? API 設(shè)計

mobx 起源于 react 生態(tài)挠阁,但由于 react 和小程序本身的差異,照搬 mobx-react 中的調(diào)用方式是不可行的溯饵。這里希望能夠盡可能地保留 react 中的調(diào)用風(fēng)格侵俗,同時兼顧小程序本身的特性,其中的取舍也可以自行定奪丰刊。最終確定的 API 格式如下隘谣。

// App.ts

const App = provider<Stores>(stores)

App<AppOptions>({
  ...
})

  
// index.ts
inject<Stores>('storeA', 'storeB')(({ storeA, storeB }) =>
  observer.page<Data, Custom>({
    onLoad() {
      this.data.storeA.count
      storeA.add()
    },
  })
)

這樣我們主要暴露三個函數(shù):provider、 observer 和 inject啄巧。provider 用于向全局綁定 stores寻歧,observer 創(chuàng)建頁面或組件,并通過 inject 向 observer 中注入需被監(jiān)聽的 store秩仆。如果對這種調(diào)用方式尚有疑問码泛,可以先看下文。

?? 內(nèi)部實現(xiàn)

整體的實現(xiàn)原理很簡單澄耍。使用 mobx 提供的 autorun 函數(shù)噪珊,autorun 會在其內(nèi)部引用的 store 中 observable 屬性發(fā)生變化時觸發(fā)回調(diào),那么在其回調(diào)中去主動觸發(fā)小程序的狀態(tài)更新即可齐莲。

由于小程序本身沒有暴露 render 相關(guān)的接口痢站,加上 wxml 中只能引用 data 中的數(shù)據(jù),唯一的選擇就是將被監(jiān)聽的數(shù)據(jù)映射至頁面或組件的 data 中选酗,并在 autorun 觸發(fā)時主動調(diào)用 setData 觸發(fā)狀態(tài)更新阵难。

需要做的工作就是在頁面(或組件)聲明時將初始 store 數(shù)據(jù)與 data 一并作為參數(shù)傳入聲明函數(shù)中,頁面掛載時調(diào)用 autorun 函數(shù)以監(jiān)聽數(shù)據(jù)變化并觸發(fā)更新芒填,最后在卸載時銷毀 autorun 釋放監(jiān)聽呜叫。

實現(xiàn)細(xì)節(jié)如下。

provider.ts

provider 用于接收一個 stores 對象并將其綁定到 App 中殿衰,stores 對象即為項目中引用的 stores 的集合怀偷,格式為 { storeA, storeB },同時 App() 的調(diào)用可以在 provider 的內(nèi)部進(jìn)行播玖。實現(xiàn)很簡單,將 stores 添加為 App 的參數(shù)屬性即可饭于。

type AppOptions<T> = WechatMiniprogram.App.Options<T>

const provider = <TStores extends AnyObject>(stores: TStores) => <TAppOptions extends AnyObject>(
  options: AppOptions<TAppOptions>
) => App({ ...options, stores })

export default provider

observer.ts

observer 是實現(xiàn)監(jiān)聽函數(shù)的核心蜀踏。小程序中的聲明函數(shù)有兩個维蒙,分別為 Page() 和 Component(),對應(yīng)頁面和組件果覆。這里以 Page() 為例颅痊。

observer 的職責(zé)為接收需要被監(jiān)聽的 stores 對象,并將其映射至頁面 data 中局待。同時觀測 stores 中屬性的變化斑响,當(dāng)觀測屬性發(fā)生變化時調(diào)用 setData() 更新頁面的狀態(tài),觸發(fā)視圖更新钳榨。

借助 mobx 提供的 autorun 函數(shù)舰罚,我們可以在 autorun 回調(diào)觸發(fā)時進(jìn)行 setData 操作。

那么思路大致為薛耻,首先將 stores 與 data 合并一同作為 options 傳入 Page 中营罢,并在頁面 onLoad 時調(diào)用 autorun,當(dāng)其回調(diào)函數(shù)觸發(fā)時立即調(diào)用 setData 將狀態(tài)更新至頁面中饼齿。同時需要在頁面 onUnload 時銷毀 autorun饲漾。

observer 調(diào)用后需要返回一個 observe 函數(shù)以接收 observedStores 對象,即需要在當(dāng)前頁面注入并監(jiān)聽的 stores缕溉。observedStores 一般需要由 inject 內(nèi)部根據(jù)外界指定 storeNames 計算并傳入考传,但亦可以在外界直接傳入 store 對象的引用。在 observe 中傳入 observedStores 并調(diào)用會進(jìn)行頁面聲明证鸥。

最終的調(diào)用格式為 observer.page(options)({ storeA, storeB })

實現(xiàn)如下僚楞。

import { autorun, IReactionDisposer } from 'mobx'
import { is, toData } from './utils'
import diff from './diff'

const observer = {
  page: <TData extends DataOption, TCustom extends CustomOption>(
    options: PageOptions<TData, TCustom>
  ) => {
    let dispose: IReactionDisposer

    const { data = {}, onLoad, onUnload } = options

    return (observedStores: AnyObject = {}) =>
      Page({
        ...options,
        data: { ...data, ...toData(observedStores) },

        onLoad(query) {
          dispose = autorun(() => {
            if (this.data) {
              const diffs: AnyObject = diff({ ...this.data, ...toData(observedStores) }, this.data)

              this.setData(diffs)
            }
          })

          if (is.fun(onLoad)) onLoad.call(this, query)
        },

        onUnload() {
          if (dispose) dispose()

          if (is.fun(onUnload)) onUnload.call(this)
        },
      })
  }
}

export default observer

注:每一次觸發(fā)更新時都將 stores 中全部屬性都更新至 data 中顯然是不可取的,這里使用 diff 對兩次狀態(tài)比對并進(jìn)行最小狀態(tài)更新以優(yōu)化性能敌土。diff 函數(shù)參考 westore 中的實現(xiàn)镜硕,diff 后的結(jié)果可以直接作為 setData 的參數(shù)傳入。

toData 的作用為將 mobx 中的 observable 對象返干,深度拷貝為一個符合小程序 data 格式的 JS 對象兴枯,具體實現(xiàn)可參照源碼中 utils.ts 文件。

Inject.ts

最后確定 inject 的實現(xiàn)思路矩欠。inject 負(fù)責(zé)兩個工作财剖,一是接收被調(diào)用時傳入的 storeNames,即需被監(jiān)聽的 store 的名稱列表癌淮,計算出對應(yīng)的 observedStores 對象躺坟,并將其傳遞給 observer 以供監(jiān)聽。二是將 provider 綁定的 stores 的直接引用通過其返回函數(shù)的調(diào)用傳遞到外界以供使用乳蓄。

結(jié)合上文的調(diào)用來看咪橙,首先 inject('storeA', 'storeB') 接收兩個 storeName,返回一個函數(shù),這個函數(shù)接收另一個函數(shù) createObserver 作為參數(shù)美侦,createObserver 的參數(shù)即是 stores 對象产舞,在這里便得到了 stores 的引用。同時 createObserver 的返回值就是 observer 調(diào)用產(chǎn)生的返回值菠剩。inject 內(nèi)部拿到 observer 返回的函數(shù)后即可在內(nèi)部調(diào)用并將 observedStores 作為參數(shù)傳入以供監(jiān)聽易猫。

最終的調(diào)用格式為 inject('storeA', 'storeB')(({ storeA, storeB }) => observer.page(options)

實現(xiàn)如下。

const mapStores = <TStores extends AnyObject>(names: (keyof TStores)[]) => (source: TStores) => {
  const target: TStores = {} as TStores

  names.forEach(key => {
    if (source && source[key]) {
      target[key] = source[key]
    }
  })

  return target
}

const inject = <TStores extends AnyObject>(...storeNames: (keyof TStores)[]) => (
  createObserver: (stores: TStores) => (observedStores: AnyObject) => void | string
) => {
  const stores = getApp().stores ?? {}
  const observedStores = mapStores(storeNames)(stores)

  return createObserver(stores)(observedStores)
}

export default inject

這里通過 getApp() 拿到 stores 的引用具壮,將其作為參數(shù)傳遞到 createObserver 中准颓。并將 storeNames 和 stores 通過 mapStores 計算得到監(jiān)聽目標(biāo)對象 observedStores。傳入 observer 產(chǎn)生的 observe 函數(shù)并調(diào)用即完成狀態(tài)注入與頁面聲明棺妓。

結(jié)束

感謝您的閱讀攘已。

項目源碼 & 文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市涧郊,隨后出現(xiàn)的幾起案子贯被,更是在濱河造成了極大的恐慌,老刑警劉巖妆艘,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彤灶,死亡現(xiàn)場離奇詭異,居然都是意外死亡批旺,警方通過查閱死者的電腦和手機幌陕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汽煮,“玉大人搏熄,你說我怎么就攤上這事∠境啵” “怎么了心例?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鞋囊。 經(jīng)常有香客問我止后,道長,這世上最難降的妖魔是什么溜腐? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任译株,我火速辦了婚禮,結(jié)果婚禮上挺益,老公的妹妹穿的比我還像新娘歉糜。我一直安慰自己,他們只是感情好望众,可當(dāng)我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布匪补。 她就那樣靜靜地躺著伞辛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叉袍。 梳的紋絲不亂的頭發(fā)上始锚,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機與錄音喳逛,去河邊找鬼。 笑死棵里,一個胖子當(dāng)著我的面吹牛润文,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播殿怜,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼典蝌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了头谜?” 一聲冷哼從身側(cè)響起骏掀,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柱告,沒想到半個月后截驮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡际度,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年葵袭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乖菱。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡坡锡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窒所,到底是詐尸還是另有隱情鹉勒,我是刑警寧澤,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布吵取,位于F島的核電站禽额,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏海渊。R本人自食惡果不足惜绵疲,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臣疑。 院中可真熱鬧盔憨,春花似錦、人聲如沸讯沈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至问慎,卻和暖如春萍摊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背如叼。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工冰木, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人笼恰。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓踊沸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親社证。 傳聞我的和親對象是個殘疾皇子逼龟,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,876評論 2 361