如何自己實現一個 mobx - 原理解析

小芋頭君發表於2017-04-25

首發於大搜車技術部落格:blog.souche.com/ru-he-zi-ji…

前言

mobx 是一個非常優雅的狀態管理庫,具有相當大的自由度,並且使用非常簡單,本文通過自己實現一個 mini 版的 mobx 來探究一下類似的 FRP 模式在 js 中的實現。

本文主要講述瞭如何自己實現一個 mobx,主要是其核心幾個 api 的實現。目的不是要重新造一個輪子,只是通過造輪子的過程,瞭解 mobx 的核心原理,以及一些具體實現的時候需要趟的坑,從而對RFP之類的程式設計正規化有更深入的瞭解。

所以,不要將此專案應用於專案中,除非你真的想節省那一點點頻寬(打包後 6K,GZip後 2.3K),用此專案瞭解 mobx 的原理即可。

另外,s-mobx 的實現和 mobx 的實現細節可能並不一致。

關於 s-mobx

github: github.com/xinyu198736…

npm: npm install s-mobx --save

核心組成

s-mobx 最最核心是兩個功能。

  1. Observable 。用來包裝一個屬性為 被觀察者
  2. autorun 。用來包裝一個方法為 觀察者

整個 s-mobx 就是圍繞這兩個功能做包裝。

依賴收集

autorun 是個神奇的函式,被他包裝過的方法,就會變為觀察者函式,並且這裡有一個很重要的特性,這個函式只會觀察自己依賴到的設為 observable 的值。

例如

autorun(function(){
    console.log(person.name);
});複製程式碼

假設person物件身上有很多個屬性是 observable 的,修改這些屬性值的時候不會觸發 autorun 包裝過的函式,只有修改 name 屬性的時候才會觸發。

這裡的原理就是依賴收集

那如何實現依賴收集呢?

這時候需要引申出一個很簡單的管理類,在 s-mobx 中,我們叫做 dependenceManager,這個工具類中管理了一個依賴的 map,結構是一個全域性唯一的 ID 和 對應的監聽的函式的陣列。

這個全域性唯一的 ID 實際上代表的就是各個被設定為 observable 的屬性值,是 Observable 類的一個屬性 obID。

當一個被 observable 包裝的屬性值發生 set 行為的時候,就會觸發 dependenceManager.trigger(obID); 從而觸發遍歷對應的監聽函式列表,並且執行,這就是 autorun 的基本原理。

那這個依賴的map 是如何收集上來的呢?

其實也很簡單,也是 dependenceManager 的操作,在執行 autorun(handler) 的時候會執行以下的程式碼(實際上也就這三句程式碼):

dependenceManager.beginCollect(handler);
handler();
dependenceManager.endCollect();複製程式碼

這裡 dependenceManager 標記現在開始收集依賴,然後執行 handler 函式,執行結束之後,標記當前收集結束。這裡的收集操作可以巢狀。具體實現見 dependenceManager。

在執行 handler 函式的時候,怎麼知道他依賴了什麼 observable 屬性值的?

這個是通過 observable 的 get 動作來實現的,每個被 observable 過的值在 get 的時候都會判斷當前是否正在收集依賴,如果是的話,就會把這個值 和 當前正在收集依賴的 handler 關聯起來儲存在 dependenceManager 中。

這就是整個 s-mobx 核心的原理。

其他的程式碼大部分只是在實現如何包裝 observable。

Observable

包裝物件值的 Observable ,核心原理是 Object.defineProperty ,給被包裝的屬性套上 get 和 set 的鉤子,在 get 中響應依賴收集,在 set 中觸發監聽函式。

陣列的包裝稍微麻煩,在 s-mobx 中使用 Proxy 來包裝,但是相容性不是很好,在 mobx 中,作者自己模擬了一個陣列物件的操作,然後包裝在原生陣列上。

另外對於 Object 物件,為其進行了遞迴包裝,每一級 Object 都繫結了一個 observable。

具體的程式碼見 s-observable (維護 Observable),s-extendObservable(包裝到具體物件屬性上)

Computed

Computed 是一種特殊的型別,他即是觀察者,也是被觀察者,然後它最大的特性是,他的計算不是每次呼叫的時候發生的,而是在每次依賴的值發生改變的時候計算的,呼叫只是簡單的返回了最後一次的計算結果。

這樣理解就明白了,其實在扮演觀察者的時候, Computed 只是 autorun 的一個變種。

Computed 中有一個方法,叫做 _reComputed,當被 computed 包裝的方法中依賴的 observable 值發生變化的時候,就會觸發 _reComputed 方法重新計算 Computed 的值。這裡的具體實現,其實就是把 _reComputed 當做 autorun 的handler 來處理,執行了一次依賴收集。

另外 Computed 還有一個特性就是可以被別人依賴,所以它也暴露了一個 get 的鉤子,在鉤子裡的操作和 observable 中的 get 鉤子做了同樣的處理。

所以,當用 @computed 包裝一個 class 的方法的時候,將其放入 autorun 中會執行兩次依賴收集,一次是收集 computed 對其他 observable 的依賴,另一次是收集 handler 對當前屬性方法的依賴。這裡 dependenceManager 提供了一種機制,可以巢狀收集依賴,採用了類似堆疊的機制。

observer

在 mobx-react 中,可以使用 @observer 對 react 物件進行包裝,使其 render 方法成為一個觀察者。

在 s-mobx 中直接整合這個功能,實現的程式碼:

var ReactMixin = {
    componentWillMount: function() {
        autorun(() => {
            this.render();
            this.forceUpdate();
        });
    }
};
function observer(target) {
    const targetCWM = target.prototype.componentWillMount;
    target.prototype.componentWillMount = function() {
        targetCWM && targetCWM.call(this);
        ReactMixin.componentWillMount.call(this);
    };
}複製程式碼

這裡給 react 元件的 prototype 做了一次 mixin,為其加入了一個 autorun,autorun的作用就是繫結元件 render 方法和其依賴的值的觀察關係。當依賴的值發生變化的時候會觸發 autorun 的引數 handler,handler中會強制執行 render() 方法和 forceUpdate()

這裡每次都強制重新渲染,沒有做很好的優化,在mobx中有個方法:isObjectShallowModified 來判斷是否需要強制重新渲染,可以考慮直接引入進來。

decorator

mobx 的最大特色就是簡單的註解使用方式,也就是 @observable @observer @computed 這些 decorator。

decorator 的實現其實很簡單,不過有些坑需要規避,例如 在 decorator 中出現的target,是class 的prototype,而不是class的例項。但是在 return 出來的 descriptor 中,set 和 get 鉤子中的this,則是 class 的例項。在實現一些複雜邏輯的時候要注意一下這個點。

具體的程式碼可以看 s-decorator

function observable(target, name, descriptor) {
    var v = descriptor.initializer.call(this);
    // 如果值是物件,為其值也建立observable
    if(typeof (v) === 'object') {
        createObservable(v);
    }
    var observable = new Observable(v);
    return {
        enumerable: true,
        configurable: true,
        get: function() {
            return observable.get();
        },
        set: function(v) {
            if(typeof (v) === 'object') {
                createObservable(v);
            }
            return observable.set(v);
        }
    };

};複製程式碼

小結

至此,一個簡化版的 mobx 基本就完成了,mobx 中常用的功能基本都做了實現。原理的話其實也很簡單,希望以後有人問起來,大家能夠說清楚 mobx 中的一些模式和實現,這就夠了。

相關文章