Mobx 原始碼解析 一(observable)

bluebrid發表於2018-09-12

前言

在Git 找到Mobx 的原始碼, 發現其是使用TypeScript 編寫,因為我對Typescrit 沒有專案經驗,所以我先會將其編譯成JavaScript,所以我們可以執行如下指令碼或者從CDN直接下載一份編譯過的原始碼,我們可以選擇umd 規範指令碼:

  1. git clone git@github.com:mobxjs/mobx.git
  2. npm i
  3. npm run quick-build

我直接從CDN 下載了一份原始碼, 然後進行分析。

Demo

首先我們從一個最基本的Demo開始,來看Mobx 的基本使用方式:

const addBtn = document.getElementById('add')const minusBtn = document.getElementById('minus')const incomeLabel = document.getElementById('incomeLabel')const bankUser = mobx.observable({ 
name: 'Ivan Fan', income: 3, debit: 2
});
const incomeDisposer = mobx.autorun(() =>
{
incomeLabel.innerText = `Ivan Fan income is ${bankUser.income
}
`
})addBtn.addEventListener('click', ()=>
{
bankUser.income ++
})minusBtn.addEventListener('click', () =>
{
bankUser.income --
})複製程式碼

我們的介面非常簡單,如圖:

Mobx  原始碼解析 一(observable)

圖1
兩個Button , 一個label. 我們在js 檔案中,我們給兩個按鈕新增了**click** 事件,事件的主體非常簡單`bankUser.income ++` `bankUser.income –`, 就是對`bankuser` 的`income` 屬性進行了自增或者自減,非常神奇, 當我們點選對應的按鈕的時候, 中間的label 的內容發生了變化。但是我們在Button 的點選事件中並沒有去操作**incomeLabel** 的內容,但是其內容確實隨著點選事件,實時發生了變化。究其原因,只有以下程式碼對**incomeLabel** 的text 進行了處理:“`const incomeDisposer = mobx.autorun(() =>
{
incomeLabel.innerText = `Ivan Fan income is ${bankUser.income
}`
})“`這就是**Mobx** 的最簡單神祕的功能,我們可以先從此開始深入研究它。

observable

從上面的JS檔案中,我們發現其中引用了mobx 兩個方法,分別是observableautorun,是的,是這樣兩個方法,讓incomeLabel 在點選按鈕的時候實時的發生了變化,所以我們接下來會對這兩個方法進行深入分析,這一章節我們會先分析observable 先進行分析。我們先開啟Mobx的原始碼, 如果我們用Vscode 開啟這個原始碼,我們可以用快捷鍵Ctrl + K Ctrl + 0 將程式碼都摺疊起來, 然後在開啟, 找到exports 的程式碼塊,我們可以檢視mobx 都暴露出了哪些方法:

Mobx  原始碼解析 一(observable)

圖2
暴露了一些列方法,我們後續會使用。

observable,翻譯成中文就是可以觀測的, 我們現在來除錯這個方法, 我們可以const bankUser = mobx.observable({ 這一行打一個斷點,然後F11,跳進去,發現原始碼對應的是一個createObservable 方法,也就是建立一個可以觀察的物件:

var observable$$1 = createObservable;
function createObservable(v, arg2, arg3) {
if (typeof arguments[1] === "string") {
return deepDecorator$$1.apply(null, arguments);

} if (isObservable$$1(v)) return v;
var res = isPlainObject$$1(v) ? observable$$1.object(v, arg2, arg3) : Array.isArray(v) ? observable$$1.array(v, arg2) : isES6Map$$1(v) ? observable$$1.map(v, arg2) : v;
if (res !== v) return res;
// otherwise, just box it fail$$1(process.env.NODE_ENV !== "production" &
&
"The provided value could not be converted into an observable. If you want just create an observable reference to the object use 'observable.box(value)'");

}複製程式碼

上面程式碼很簡單,引數有三個,但是我們在呼叫的時候,值傳遞了一個引數, 所以我們暫且只要關心第一個引數 r .以下是這個functin 的基本邏輯:

  1. 如果傳入的第二個引數是一個字串, 則直接呼叫deepDecorator$$1.apply(null, arguments);
  2. 判斷第一個引數是否已經是一個可觀察的物件了,如果已經是可觀察的物件了,就直接返回這個物件
  3. 判斷第一個引數是什麼型別,然後呼叫不同的方法, 總共有三種型別: object , array , map (ES 的Map 資料型別), 分別呼叫:observable$$1.object, observable$$1.array, observable$$1.map方法, 那這個observable$$1又是什麼呢?在第一行var observable$$1 = createObservable;
    表面就是createObservable方法。但是這個方法就短短几行程式碼,並沒有object, array, map著三個方法, 我們發現在這個方法下面有observableFactories 物件,其是一個工廠物件,用來給createObservable新增方法,其定義了這三個方法,並且通遍歷過Object.keys(observableFactories).forEach(function (name) {
    return (observable$$1[name] = observableFactories[name]);

    });

因為在我們的Demo 中我們傳遞的是一個Object, 所以會呼叫observable$$1.object 方法,接下來我們在繼續分析這個方法, 其程式碼如下:

    object: function (props, decorators, options) { 
if (typeof arguments[1] === "string") incorrectlyUsedAsDecorator("object");
var o = asCreateObservableOptions$$1(options);
if (o.proxy === false) {
return extendObservable$$1({
}, props, decorators, o);

} else {
var defaultDecorator = getDefaultDecoratorFromObjectOptions$$1(o);
var base = extendObservable$$1({
}, undefined, undefined, o);
var proxy = createDynamicObservableObject$$1(base);
extendObservableObjectWithProperties$$1(proxy, props, decorators, defaultDecorator);
return proxy;

}
},複製程式碼

var o = asCreateObservableOptions$$1(options);
生成的了一個簡單的物件:

var defaultCreateObservableOptions$$1 = { 
deep: true, name: undefined, defaultDecorator: undefined, proxy: true
};
複製程式碼

o.proxy 的值為true, 所以會走else 邏輯分支, 所以接下來我們一一分析else 分支中的每一條程式碼。

  1. var defaultDecorator = getDefaultDecoratorFromObjectOptions$$1(o);
    這個是跟裝飾器有關的邏輯,我們先跳過
  2. var base = extendObservable$$1({
    }, undefined, undefined, o);
    o物件進行了加工處理,變成了一個Symbol 資料型別。

這一步操作非常重要,給一個空物件新增了一個$mobx$$1(var $mobx$$1 = Symbol("mobx administration");
)的屬性, 其值是一個 ObservableObjectAdministration 型別物件,其write 方法在後續資料攔截中會呼叫。

Mobx  原始碼解析 一(observable)

圖3
  1. var proxy = createDynamicObservableObject$$1(base);
    這個方法,最為核心, 對這個物件進行了代理(Proxy)
Mobx  原始碼解析 一(observable)

圖4

對這個物件的屬性的get, set, has, deleteProperty, ownKeys, preventExtensions方法進行了代理攔截,這個是Mobx 事件資料新增的一個核心點。

  1. 第三點的proxy 其實只是初始化了一個簡單的代理物件,但是沒有與我們需要觀察的target(也就是mobx.observable方法傳遞進來的需要被觀察的物件)關聯起來, extendObservableObjectWithProperties$$1(proxy, props, decorators, defaultDecorator);
    方法會遍歷target 的屬性,將其賦值給proxy物件, 然後我們mobx.observable 裡的物件都被代理了,也就是實現了對屬性操作的攔截處理。

  2. 在第四點extendObservableObjectWithProperties$$1 方法中, 最終會給原始的物件的屬性進行裝飾,通過檢視function 的 call stack 得知,最後對呼叫ObservableObjectAdministration 的addObservableProp 方法, 針對每一個propName(原始物件的Key)生成一個ObservableValue 物件,並且儲存在ObservableObjectAdministration 物件的values

Mobx  原始碼解析 一(observable)

圖三中發現, 真正實現資料攔截的就是objectProxyTraps 攔截器, 下一章節,我們需要對這個攔截器進行深入分析,著重看get,set如何實現了資料攔截。

  1. return proxy;
    最終將返回一個已經被代理過的物件,替換原生物件。

bankUser 物件就是一個已經被代理了的物件,並且包含了一個Symbol 型別的新的屬性。

const bankUser = mobx.observable({ 
name: 'Ivan Fan', income: 3, debit: 2
});
複製程式碼

總結

  1. observable 首先傳入一個原始物件(可以傳入多種型別的資料: array, map, object, 現在只分析object 型別的情況)
  2. 建立一個空的Object 物件,並且新增一些預設屬性(var base = extendObservable$$1({
    }, undefined, undefined, o);
    ), 包括一個Symbol型別的屬性,其值是一個ObservableObjectAdministration 型別的物件.
  3. 將這個物件用ES6Proxy 進行了代理, 會攔截這個物件的一些列操作(get, set…) var proxy = new Proxy(base, objectProxyTraps);
  4. 將原始物件,進行遍歷,將其所有的自己的屬性掛載在新建立的空物件中
  5. 返回已經加工處理的物件bankUser
  6. 後續就可以監聽這個物件的相應的操作了。
  7. 加工後的物件如下圖所示, 後面操作的物件,就是如下這個物件,但是observable 方法,其實只是做到了如下圖的第二步(2), 第三步(3)的observers屬性還是一個沒有任何值的Set 物件,在後續分析autorun 方法中,會涉及到在什麼時候去給它賦值
Mobx  原始碼解析 一(observable)

來源:https://juejin.im/post/5b9733036fb9a05d265942fc

相關文章