基於 MobX 構建檢視框架無關的資料層-與 Vue 的結合(1)

kuitos發表於2018-11-12

mobx-vue 目前已進入 mobxjs 官方組織,歡迎試用求 star!

原文連結

幾周前我寫了一篇文章描述了 mobx 與 angularjs 結合使用的方式及目的 (老樹發新芽—使用 mobx 加速你的 AngularJS 應用),這次介紹一下如何將 MobX 跟 Vue 結合起來。

安裝

npm i mobx-vue -S
複製程式碼

使用

mobx-vue 的使用非常簡單,只需要使用 connect 將你用 mobx 定義的 store 跟 vue component 連線起來即可:

<template>
    <section>
        <p v-text="amount"></p>
        <p v-for="user in users" :key="user.name">{{user.name}}</p>
    </section>
</template>

<script lang="ts">
    import { Connect } from "mobx-vue";
    import Vue from "vue";
    import Component from "vue-class-component";
    class ViewModel {
        @observable users = [];
        @computed get amount() { return this.users.length }
        @action fetchUsers() {}
    }

    @Connect(new ViewModel())
    @Component()
    export default class App extends Vue {
        mounted() { 
            this.fetchUsers();
        }
    }
</script>
複製程式碼

Why MobX/mobx-vue

我們知道,mobx 跟 vue 都是基於 資料劫持&依賴收集 的方式來實現響應式機制的。mobx 官方也多次提到 inspired by vue,那麼我們為什麼還要將兩個幾乎一樣的東西結合起來呢?

Yes, it`s weird.

2016年我在構建公司級元件庫的時候開始思考一個問題,我們如何在程式碼庫基於某一框架的情況下,能以儘可能小的代價在未來將元件庫遷移到其他 框架/庫 下?總不能基於新的技術全部重寫一遍吧,這也太浪費生命了。且不說對於基礎控制元件而言,互動/行為 邏輯基本上是可確定的,最多也就是 UI 上的一些調整,而且單純為了嘗試新技術耗費公司人力物力將基礎庫推導重寫也是非常不職業的做法。那麼我們只能接受被框架綁架而只能深陷某一技術棧從此泥潭深陷嗎?對於前端這種框架半衰期尤其短的領域而言顯然是不可接受的,結果無非就是要麼自己跑路坑後來人,要麼招不到人來一起填坑… 簡單來說我們無法享受新技術帶來的種種紅利。

在 MVVM 架構視角下,越是重型的應用其複雜度越是集中在 M(Model) 跟 VM(ViewModel) 這兩層,尤其是 Model 層,理論上應該是能脫離上層檢視獨立執行獨立釋出獨立測試的存在。而相應的不同檢視框架只是使用了不同繫結語法的動態模板引擎而已,這個觀點我在前面的幾篇文章裡都講述過。所以只要我們將檢視層做的很薄,我們遷移的成本自然會降到一個可接受的範疇,甚至有可能通過工具在編譯期自動生成不同框架的檢視層程式碼。

要做到 Model 甚至 ViewModel 獨立可複用,我們需要的是一種可以幫助我們描述各資料模型間依賴關係圖且框架中立的通用狀態管理方案。這期間我嘗試過 ES6 accessor、redux、rxjs 等方案,但都不盡如人意。accessor 過於底層且非同步不友好、redux 開發體驗太差(參考Redux資料流管理架構有什麼致命缺陷,未來會如何改進?)、rxjs 過重等等。直到後來看到 MobX:MobX 語法足夠簡單、弱主張(unopinioned)、oop 向、框架中立等特性正好符合我的需求。

在過去的一年多裡,我分別在 react、angularjs、angular 上嘗試過基於 MobX 構建 VM/M 層,其中有兩個上線專案,一個個人專案,實踐效果基本上也達到了我的預期。在架構上,我們只需要使用對應的 connector,就能基於同一資料層,在不同框架下自如的切換。這樣看來,這套思路現在就剩 Vue 沒有被驗證了。

在 mobx-vue 之前,社群已經有一些優秀的 connector 實現,如 movue vue-modex 等,但基本都是基於 vue 的外掛機制且 inspired by vue-rx,除了使用起來相對繁瑣的問題外,最大的問題是其實現基本都是藉助 Vue.util.defineReactive 來做的,也就是說還是基於 Vue 自有的響應式機制,這在一定程度不僅浪費了 MobX 的reactive 能力,而且會為遷移到其他檢視框架下埋下了不確定的種子(畢竟你無法確保是 Vue 還是 MobX 在響應狀態變化)。

參考:why mobx-vue

理想狀態下應該是由 mobx 管理資料的依賴關係,vue 針對 mobx 的響應做出 re render 動作即可,vue 只是一個單純的動態模板渲染引擎,就像 react 一樣。

在這樣的一個背景下,mobx-vue 誕生了。

mobx-vue 是如何運作的

既然我們的目的是將 vue 變成一個單純的模板渲染引擎(vdom),並且使用 mobx 響應式機制取代 vue 的響應式,那麼只要我們劫持到 Vue 的元件裝載及更新方法,然後在元件裝載的時候收集依賴,在依賴發生變更時更新元件即可。

以下內容與其叫做 mobx-vue 是如何運作的,不如叫 Vue 原始碼解析?:

我們知道 Vue 通常是這樣初始化的:

new Vue({ el: `#app`, render: h => h(App)});
複製程式碼

那麼找到 Vue 的建構函式,

function Vue (options) {
  ......
  this._init(options)
}
複製程式碼

跟進到_init方法,除了一系列元件初始化行為外,最關鍵是最後一部分的 $mount 邏輯:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
複製程式碼

跟進 $mount 方法,以 web runtime 為例:

if (process.env.NODE_ENV !== `production` && config.performance && mark) {
    updateComponent = () => {
        ...
    }
} else {
    updateComponent = () => {
        vm._update(vm._render(), hydrating)
    }
}

vm._watcher = new Watcher(vm, updateComponent, noop)
複製程式碼

從這裡可以看到,updateComponent 方法將是元件更新的關鍵入口,跟進 Watcher 建構函式,看 Vue 怎麼呼叫到這個方法的:

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    ...
    this.expression = process.env.NODE_ENV !== `production`
      ? expOrFn.toString()
      : ``
    // parse expression for getter
    if (typeof expOrFn === `function`) {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      ...
    }
    this.value = this.lazy
      ? undefined
      : this.get()
複製程式碼
get () {
    ...
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
  }
複製程式碼

看到這裡,我們能發現,元件 裝載/更新 的發起者是: value = this.getter.call(vm, vm) ,而我們只要通過 vm._watcher.getter 的方式就能獲取相應的方法引用, 即 updateComponent := vm._watcher.getter。所以我們只要在 $mount 前將 MobX 管理下的資料植入元件上下文供元件直接使用,在$mount 時讓 MobX 收集相應的依賴,在 MobX 檢測到依賴更新時呼叫 updateComponent 即可。這樣的話既能讓 MobX 的響應式機制通過一種簡單的方式 hack 進 Vue 體系,同時也能保證元件的原生行為不受到影響(生命週期鉤子等)。

中心思想就是用 MobX 的響應式機制接管 Vue 的 Watcher,將 Vue 降級成一個純粹的裝載 vdom 的元件渲染引擎。

核心實現很簡單:

const { $mount } = Component.prototype;

Component.prototype.$mount = function (this: any, ...args: any[]) {
    let mounted = false;
    const reactiveRender = () => {
        reaction.track(() => {
            if (!mounted) {
                $mount.apply(this, args);
                mounted = true;
            } else {
                this._watcher.getter.call(this, this);
            }
        });

        return this;
    };
    const reaction = new Reaction(`${name}.render()`, reactiveRender);
    dispose = reaction.getDisposer();
    return reactiveRender();
};
複製程式碼

完整程式碼在這裡:https://github.com/mobxjs/mobx-vue/blob/master/src/connect.ts

最後

尤大大之前說過:mobx + react 是更繁瑣的 Vue,本質上來看確實是這樣的,mobx + react 組合提供的能力恰好是 Vue 與生俱來的。而 mobx-vue 做的事情則剛好相反:將 Vue 降級成 react 然後再配合 MobX 升級成 Vue ?。這確實很怪異。但我想說的是,我們的初衷並不是說 Vue 的響應式機制實現的不好從而要用 MobX 替換掉,而是希望藉助 MobX 這個相對中立的狀態管理平臺,面向不同檢視層技術提供一種相對通用的資料層程式設計正規化,從而儘量抹平不同框架間的語法及技術棧差異,以便為開發者提供更多的檢視技術的決策權及可能性,而不至於被某一框架綁架裹挾。

PS: 這篇是系列文章的第一篇,後面將有更多關於 如何基於 MobX 構建檢視框架無關的資料層 的架構正規化及實踐的內容,敬請期待!

相關文章