【用故事解讀 MobX 源碼(五)】 Observable

================前言===================html

=======================================git

A. Story Time

最高警長看完執行官(MobX)的自動部署方案,對 「觀察員」 這個基層人員工做比較感興趣,自執行官拿給他部署方案的時候,他就注意到全部上層人員的功能都是基於該底層人員高效的工做機制;github

次日,他找上執行官(MobX)一塊兒去視察「觀察員」所在機構部門(下面簡稱爲 」觀察局「),想更深刻地瞭解 「觀察員」 運行分配機制。算法

observable

當最高警長到達部門的時候,剛好遇到該部門剛好要開始執行 MobX 前不久新下發的任務,要求監控 parent 對象的一舉一動:編程

var parent = {
  child: {
    name: 'tony',
    age: 15
  }
  name: 'john'
}

var bankUser = observable(parent);

任務達到觀察局辦公室後,相應的辦公室文員會對任務進行分析,而後會依據對象類型交給相應科室進行處理,常見的有 object 科,另外還有 map 科和 array 科;segmentfault

如今,辦公室文員見傳入的對象是 parent 是個對象,就將其傳遞給 object 科,讓其組織起一塊兒針對該 parent 對象的 」觀察小組「,組名爲 bankUser設計模式

object 科接到任務,委派某位科長(如下稱爲 bankUser 科長)組成專項負責此 parent 對象的觀察工做,bankUser 科長接手任務後發現有兩個屬性,其中 child 是對象類型,age 是原始值類型,就分別將任務委派給 child 小科長 和 name 觀察員 O1,child 小科長接到任務後再委派給 name 觀察員 O2 和 age 觀察員 O3,最終執行該任務的人員結構以下:api

structor

觀察員的任務職責咱們已經很熟悉了,當讀寫觀察員對應的數據時將觸發 reportObservedpropagateChanged 方法;數組

這裏涉及到兩位科長(bankUser 科長 和 child 小科長),那麼科長的任務職責是什麼呢?

科長的人物職責是起到 管理 做用,它負責統管在他名下的觀察員。好比當咱們讀寫 bankUser.child 對象的 name 屬性時(好比執行語句 bankUser.child.name = 'Jack'),首先感知到讀寫操做的並不是是 觀察員 O2 而是bankUser科長bankUser科長會告知 child 小科長有數據變動,child 小科長而後再將信息傳達給 name 觀察員 O2 ,而後纔是觀察員 O2 對數據讀寫起反應,這才讓觀察員 O2 發揮做用。

transform

從代碼層面看,咱們看到僅僅是執行 bankUser.child.name = 'Jack'這一行語句,和咱們日常修改對象屬性並沒有二致。然而在這一行代碼背後其實牽動了一系列的操做。這實際上是 MobX 構建起的一套 」鏡像「 系統,使用者仍舊按平時的方式讀寫對象,然而每一個屬性的讀寫操做實則都鏡像到觀察局 的某個小組具體的操做;很是相似於古代的 」垂簾聽政「 ,看似皇帝坐在文武百官前面,其實真正作出決策響應的是藏在簾後面的那我的。

前幾章中咱們只看到觀察員在活動,然則背後離不開 科長 這一角色機制在背後暗暗的調度。對每項任務,最終都會落實到觀察員採起「一對一」模式監控分配到給本身的觀察項,而每一個觀察員確定是隸屬於某個 」科長「 帶領。在 MobX 系統裏,辦公室、科長和觀察員是密不可分,共同構建起 觀察局 運行體制;

"分工明確,運轉高效",這是最高警長在巡視完觀察員培訓基地後的第一印象,觀察局運轉的每一步的設計都有精細的考量;

B. Source Code Time

先羅列本文故事中人物與 MobX 源碼概念映射關係:

故事人物 MobX 源碼 解釋
警署最高長官 (無) MobX 用戶,沒錯,就是你
執行官 MobX MobX 整個 MobX 運行環境
觀察局辦公室(主任、文員) observableobservable.box 用於建立 Observable 的 API
object 科室、map 科室、array 科室 observable.objectobservable.mapobservable.array 將不一樣複合類型轉換成觀察值的方法
科長 ObservableObjectAdministration 主要給對象添加 $mobx 屬性
觀察員 ObservableValue 實例 ObservableValue 實例

一、總入口:observable

observable 對應上述故事中的 觀察局辦公室主任 角色,自己不提供轉換功能,主要是起到統一調度做用 —— 這樣 MobX 執行官只須要將命令發給辦公室人員就行,至於內部具體的操做、具體由哪一個科室處理,MobX 執行官不須要關心。

將與 observable 的源碼 相關的源碼稍微整理,就是以下的形式:

var observable = createObservable;
// 使用「奇怪」的方式來擴展 observable 函數的功能,就是將 observableFactories 的方法挨個拷貝給 observable
Object.keys(observableFactories).forEach(function(name) {
  return (observable[name] = observableFactories[name]);
});
  • 首先 observable 是函數,函數內容就是 createObservable
  • 其次 observable 是對象,對象屬性和 observableFactories 一致

也就是說 observable 實際上是 各類構造器的總和,整合了 createObservable(默認構造器) + observableFactories(其餘構造器)

本身也能夠在 console 控制檯中打印來驗證一番:

const { observable } = mobx;

console.log('observable name:', observable.name);
console.log(Object.getOwnPropertyNames(observable));

從如下控制檯輸出的結果來看,observable 的屬性的確來自於createObservableobservableFactories 這二者:
ob value

文字比較枯燥,用圖來表示就是下面那樣子:

observable

這裏我大體劃分了一下,分紅 4 部份內容來理解:

  • 第一部分:createObservable 方法剛纔粗略講過,是 MobX API 的 observable 的別名,是一個高度封裝的方法,算是一個總入口,方便用戶調用;該部分對應上述故事中的 觀察局辦公室主任 的角色
  • 第二部分:box 是一個轉換函數,用於將 原值(primitive value) 直接轉換成 ObservableValue 對象;shallowBoxbox 函數的非 deep 版本;該部分對應上述故事中的 觀察局辦公室文員 的角色;
  • 第三部分:針對 object、array 以及 map 這三種數據類型分別提供轉換函數,同時也提供 shallow 的版本;該部分對應上述故事中的 科室 部分;
  • 第四部分:提供四種裝飾器函數,裝飾器的概念咱們上一節課講過,主要輔助提供裝飾器語法糖做用;對普通 MobX 用戶來說這部分平時也是接觸不到的;

如何理解這 4 部分的以前的關係呢?我我的的理解以下:

  • 第三部分屬於 「基層建築」,分別爲 object、array 以及 map 這三種數據類型提供轉換成可觀察值的功能(默認是遞歸轉換,shallow 表示非遞歸轉換);這部分對應上述故事中的科室概念,不一樣的觀察任務由不一樣的科室來處理;
  • 第一部分和第二部分屬於 「上層建築」,提供統一的接口,具體的轉換功能都是調用第三部分中的某個轉換函數來實現的;這兩部分對應上述故事中的 觀察局辦公室 部分。
  • 第一部分咱們最熟悉,不過第二部分的 box 函數轉換能力反而比第一部分更廣,支持將原始值轉換成可觀察值
  • 第四部分和另外三部分沒有直接的關係,主要輔助提供裝飾器函數;注意,沒有直接的聯繫並不表明沒有聯繫,第四部分中裝飾器內的核心邏輯和另外三部分是同樣的(好比都調用 decorator 方法)。

下面咱們看兩個具體的示例,來輔助消化上面的結論。

示例一observable.box(obj) 底層就是調用 observable.object(obj)實現的

var user = {
  income: 3,
  name: '張三'
};
var bankUser = observable.object(user);
var bankUser2 = observable.box(user);

console.log(bankUser);
console.log(bankUser2);

box and object
能夠發現 bankUser2 中的 value 屬性部份內容和 bankUser 是如出一轍的。

示例二observable.box(primitive) 能行,observable(primitive) 卻會報錯

var pr1 = observable.box(2);
console.log(pr1);
console.log('--------華麗分割-----------')
var pr2 = observable(2);
console.log(pr2);

從報錯信息來看,MobX 會友情提示你改用 observable.box 方法實現原始值轉換:

box can

二、第一部分:createObservable

正如上面所言,該函數其實就是 MobX API 的 observable 的 「別名」。因此也是對應上述故事中的 觀察局辦公室主任 角色;

該函數自己不提供轉換功能,只是起到 "轉發" 做用,將傳入的對象轉發給對應具體的轉換函數就好了;

看一下 源碼

function createObservable(v, arg2, arg3) {
  // 走向 ①
  if (typeof arguments[1] === 'string') {
    return deepDecorator.apply(null, arguments);
  }
  
  // 走向 ②
  if (isObservable(v)) return v;
  
  var res = isPlainObject(v)
    ? observable.object(v, arg2, arg3) // 走向③
    : Array.isArray(v)
      ? observable.array(v, arg2)  // 走向 ④
      : isES6Map(v) ? observable.map(v, arg2) // 走向 ⑤
      : v;
  
  if (res !== v) return res;
  // 走向 ⑥
  fail(
        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)'`
    )
}

不難看出實際上是典型的採用了 策略設計模式 ,將多種數據類型(Object、Array、Map)狀況的轉換封裝起來,好讓調用者不須要關心實現細節:

該設計模式參考可參考 深刻理解JavaScript系列(33):設計模式之策略模式

用圖來展現一下具體的走向:

trend

  • 走向 ① 是 裝飾器語法所特有的,這是由於此時傳給 createObservable 的第二個參數是 string 類型,這一點咱們在上一篇文章有詳細論述;
  • 走向 ② 很直觀,若是傳入的參數就已是 觀察值 了,很少廢話直接返回傳入的值,不須要轉換;
  • 走向 ③、④ 、⑤ 是直根據傳入參數的類型分別調用具針對具體類型的轉換方法;
  • 走向 ⑥,在上面示例中咱們已經看到過, 針對原始值會提示建議用戶使用 observable.box 方法。

第一部分的 createObservable 的內容就那麼些,總之只是起了 「嚮導」 做用。是否是比你想象中的要簡單?

接下來咱們繼續看第二部分的 observable.box 方法。

三、第二部分:observable.box

這個方法對應上述故事中的 觀察局辦公室文員 角色,也是屬於辦公室部門的,所起到的做用和 主任 大同小異,只是平時咱們用得並很少罷了。

當我第一次閱讀 官網文檔 中針對有關 observable.box 的描述時:

box

來回讀了幾回,「盒子」是個啥?它幹嗎用的? 「observable」 和 「盒子」 有半毛錢關係?

直到看完該函數的詳細介紹 boxed values 後,方纔有所感悟,這裏這 box 方法就是將普通函數 「包裝」 成可觀察值,因此 box 是動詞而非名詞

準確地理解,observable.box 是一個轉換函數,好比咱們將普通的原始值 "Pekin"(北京)轉換成可觀察值,就可使用:

const cityName = observable.box("Pekin");

原始值 "Pekin" 並不具有可觀察屬性,而通過 box 方法操做以後的 cityName 變量具備可觀察性,好比:

console.log(cityName.get());
// 輸出 'Pekin'

cityName.observe(function(change) {
    console.log(change.oldValue, "->", change.newValue);
});

cityName.set("Shanghai");
// 輸出 'Pekin -> Shanghai'

從輸入輸出角度來看,這 box 其實就是將普通對象轉換成可觀察值的過程,轉換過程當中將一系列能力「添加」到對象上,從而得到 「自動響應數值變化」 的能力。

那麼具體這 box 函數是如何實現的呢?直接看 源碼

box: function(value, options) {
  if (arguments.length > 2) incorrectlyUsedAsDecorator('box');
  var o = asCreateObservableOptions(options);
  return new ObservableValue(
    value,
    getEnhancerFromOptions(o),
    o.name
  );
}

發現該方法僅僅是調用 ObservableValue 構造函數,因此 box 方法操做的結果是返回 ObservableValue 實例。

這裏的 asCreateObservableOptions 方法僅僅是格式化入參 options 對象而已。

四、核心類:ObservableValue

總算是講到這個 ObservableValue 類了,該類是理解可觀察值的關鍵概念。這個類對應上述故事中的 觀察員 角色,就是最基層的 name 觀察員 O一、O二、O3 那些。

本篇文章的最終目的也就是爲了講清楚這個 ObservableValue 類,其餘的概念反而是圍繞它而建立起來的。

分析其源碼,將這個類的屬性和方法都拎出來瞧瞧,繪製成類圖大體以下:

gene

你會發現該類 繼承自 Atom 類,因此在理解 ObservableValue 以前必須理解 Atom

其實在 3.x 版本的時候, ObservableValue 繼承自 BaseAtom
隨着升級到 4.x 版本,官方以及廢棄了 BaseAtom,直接繼承自 Atom 這個類。

4.一、Atom

在 MobX 的世界中,任何可以 存儲並管理 狀態的對象都是 Atom,故事中的 觀察員(ObservableValue 實例)本質上就是 Atom(準確的說,而 ObservableValue 是繼承了 Atom 這個基類),Atom實例有兩項重大的使命:

  1. 當它的值被使用的時候,就會觸發 reportObserved 方法,在 第一篇文章 的講解中可知,MobX 正是基於該方法,使得觀察員和探長之間創建關聯關係。
  2. 當它的值受到更改的時候,將會觸發 reportChanged 方法,在第三篇文章 《【用故事解讀 MobX源碼(三)】 shouldCompute》中可知,基於該方法觀察員就能夠將 非穩態信息逐層上傳,最終將讓探長、會計員從新執行任務。

Atom 類圖以下,從中咱們看到前面幾章中所涉及到的 onBecomeUnobservedonBecomeObservedreportObservedreportChanged 這幾個核心方法,它們都來源於 Atom 這個類:
Atom

因此說 Atom 是整個 MobX 的基石並不爲過,全部的自動化響應機制都是創建在這個最最基礎類之上。正如在大天然中,萬物都是由原子(atom)構成的,藉此意義, MobX 中的 」具有響應式的「 對象都是由這個 Atom 類構成的。
ComputeValue類 也繼承自 AtomReaction 類的實現得依靠 Atom,所以不難感知 Atom 基礎重要性)

4.二、createAtom

理論上你只要建立一個 Atom 實例就能融入到 mobx 的響應式系統中,

如何本身建立一個 Atom 呢?

MobX 已經暴露了一個名爲 createAtom 方法,
官方文檔 建立 observable 數據結構和 reactions(反應) 給出了建立一個 鬧鐘 的例子,具體講解了該 createAtom 方法的使用:

...
  // 建立 atom 就能和 MobX 核心算法交互
  this.atom = createAtom(
      // 第一個參數是 name 屬性,方便後續 
      "Clock",
      // 第二個參數是回調函數,可選,當 atom 從 unoberved 狀態轉變到 observed 
      () => this.startTicking(),
      // 第三個參數也是回調函數,可選,與第二個參數對應,此回調是當 atom 從 oberved 狀態轉變到 unobserved 時會被調用
      // 注意到,同一個 atom 有可能會在 oberved 狀態和 unobserved 之間屢次轉換,因此這兩個回調有可能會屢次被調用
      () => this.stopTicking()
  );
...

同時文中也給出了對應的最佳實踐:

  • 最好給建立的 Atom 起一個名字,方便後續 debug
  • onBecomeObservedonBecomeUnobserved 和咱們面向對象中構造函數與析構函數的做用類似,方便進行資源的申請和釋放

不過 Atom 實例這個仍是偏向底層實現層,除非須要強自定義的特殊場景中,平時咱們推薦直接使用 observable 或者 observable.box 來建立觀察值更爲簡單直接;

4.三、理解 ObservableValue

MobX 在 Atom 類基礎上,泛化出一個名爲 ObservableValue 類,就是咱們耳熟能詳的 觀察值 了。從代碼層面上來看,實現 ObservableValue 其實就是繼承一下 Atom 這個類,而後再添加許多輔助的方法和屬性就能夠了。

理解完上述的 Atom 對象以後,你就已經理解 ObservableValue 的大部分。接下來就是去理解 ObservableValue 相比 Atom 多出來的屬性和方法,我這裏並不會全講,太枯燥了。只挑選重要的兩部分 —— Intercept & Observe 部分 和 enhancer 部分

4.3.一、Intercept & Observe 部分

ObservableValue 類圖中除了常見的 toJSON()toString() 方法以外,有兩個方法格外引人注目 —— intercept()observe 兩個方法。

若是把 「對象變動」 做爲事件,那麼咱們能夠在 事件發生以前事件方法以後 這兩個 「切面」 分別能夠安插回調函數(callback),方便程序動態擴展,這屬於 面向切面編程的思想

不瞭解 AOP 的,能夠查閱 知乎問答-什麼是面向切面編程AOP?

在 MobX 世界裏,將安插在 事件發生以前 的回調函數稱爲 intercept,將安插在 事件發生以後 的回調函數稱爲 observe。理解這兩個方法能夠去看 官方中的示例,能快速體會其做用。

這裏稍微進一步講細緻一些,有時候官方文檔會中把 intercept 理解成 攔截器。 這是由於它做用於事件(數據變動)發生以前,所以能夠操縱變動的數據內容,甚至能夠經過返回 null 忽略某次數據變化而不讓它生效。

其做用機制也很直接,該方法調用的最終都是調用實例的 intercept 方法,這樣每次在值變動以前(如下 prepareNewValue 方法執行),都會觸發觀察值上所綁定的全部的 攔截器

ObservableValue.prototype.prepareNewValue = function(newValue) {
  ...
  if (hasInterceptors(this)) {
    var change = interceptChange(this, {
      object: this,
      type: 'update',
      newValue: newValue
    });
    if (!change) return UNCHANGED;
    newValue = change.newValue;
  }
  // apply modifier
  ...
};

着重裏面的那行語句 if (!change) return UNCHANGED; ,若是你在 intercept 安插的回調中返回 null 的話,至關於告知 MobX 數值沒有變動(UNCHANGED),既然值沒有變動,後續的邏輯就不會觸發了。

observe 的做用是將回調函數安插在值變動以後(如下 setNewValue 方法調用),一樣是經過調用 notifyListeners 通知全部的監聽器

ObservableValue.prototype.setNewValue = function(newValue) {
  ...
  this.reportChanged();
  if (hasListeners(this)) {
    notifyListeners(this, {
      type: 'update',
      object: this,
      newValue: newValue,
      oldValue: oldValue
    });
  }
};

==========【如下是額外的知識內容,可跳過,不影響主線講解】===========

如何解除安插的回調函數?

Intercept & Observe 這兩個函數返回一個 disposer 函數,這個函數是 解綁函數,調用該函數就能夠取消攔截器或者監聽器 了。這裏有一個最佳實踐,若是不須要某個攔截器或者監聽器了,記得要及時清理本身綁定的監聽函數 永遠要清理 reaction —— 即調用 disposer 函數。

那麼如何實現 disposer 解綁函數這套機制?

以攔截器(intercept)爲例,註冊的時候調用 registerInterceptor 方法:

function registerInterceptor(interceptable, handler) {
  var interceptors =
    interceptable.interceptors || (interceptable.interceptors = []);
  interceptors.push(handler);
  return once(function() {
    var idx = interceptors.indexOf(handler);
    if (idx !== -1) interceptors.splice(idx, 1);
  });
}

總體的邏輯比較清晰,就是將傳入的 handler(攔截器)添加到 interceptors 數組屬性中。關鍵是在於返回值,返回的是一個閉包 —— once 函數調用的結果值。

因此咱們簡化一下 disposer 解綁函數的定義:

disposer = once(function() {
  var idx = interceptors.indexOf(handler);
  if (idx !== -1) interceptors.splice(idx, 1);
});

恰是這個 once 函數是實現解綁功能的核心

查看這個 once 函數源碼只有寥寥幾行,卻將閉包的精髓運用到恰到好處。

function once(func) {
  var invoked = false;
  return function() {
    if (invoked) return;
    invoked = true;
    return func.apply(this, arguments);
  };
}

once 方法其實經過 invoked 變量,控制傳入的 func 函數只調用一次。

回過頭來 disposer 解綁函數,調用一次就會從 interceptors 數組中移除當前攔截器。使用 once 函數後,你不管調用多少次 disposer 方法,最終都只會解綁一次。

因爲 once 是純函數,所以大夥兒能夠提取出來運用到本身的代碼庫中 —— 這也是源碼閱讀的益處之一,借鑑源碼中優秀部分,而後學習吸取,引覺得用。

=======================================================

4.3.二、enhancer 部分

這部分是在 ObservableValue 構造函數中發揮做用的,其影響的偏偏是最核心的數據屬性:

function ObservableValue(value, enhancer, name, notifySpy) {
      ...
      _this.enhancer = enhancer;
      _this.value = enhancer(value, undefined, name);
      ...
    }

在上一篇文章《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》中有說起過 enhance,在那裏咱們提及過 enhance 其實就是裝飾器(decorator)的有效成分,該有效成分影響的正是本節所講的 ObservableValue 對象。結合 types/modifier.ts 中有各類 Enhancer 的具體內容,就能大體瞭解 enhancer 是如何起到 轉換數值 的做用的,以常見的 deepEnhancer 爲例,當在構造函數中執行 _this.value = enhancer(value, undefined, name); 的時候會進入到 deepEnhance 函數體內:

function deepEnhancer(v, _, name) {
  // it is an observable already, done
  if (isObservable(v)) return v;
  // something that can be converted and mutated?
  if (Array.isArray(v))
    return observable.array(v, {
      name: name
    });
  if (isPlainObject(v))
    return observable.object(v, undefined, {
      name: name
    });
  if (isES6Map(v))
    return observable.map(v, {
      name: name
    });
  return v;
}

這段代碼是否似曾相識?!沒錯,和上一節所述 createObservable 方法幾乎同樣,採用 策略設計模式 調用不一樣具體轉換函數(好比 observable.object 等)。

如今應該可以明白,第一部分的 createObservable 和 第二部分的 observable.box 都是創建在第三部分之上,並且經過第一部分、第二部分以及第三部分得到的觀察值對象都是屬於觀察值對象(ObservableValue),大同小異,頂多只是「外形」有略微的差異。

經過該 enhancer 部分的講解,咱們發現全部待分析的重要部分都聚焦到第三部分的 observable.object 等這些個轉換方法身上了。

五、第三部分:observable.object

由於結構的緣由,上面先講了最基層的 ObservableValue 部分,如今回來說的 observable.object 方法。從這裏你能大概體會到 MobX 體系中遞歸現象new ObservableValue 裏面會調用 observable.object 方法,從後面的講解裏你將會看到 observable.object 方法裏面也會調用 new ObservableValue 的操做,因此 遞歸地將對象轉換成可觀察值 就很瓜熟蒂落。

閱讀官方文檔 Observable.object,該 observable.object 方法就是把一個普通的 JavaScript 對象的全部屬性都將被拷貝至一個克隆對象並將克隆對象轉變成可觀察的,並且 observable 是 遞歸應用 的。

observable.object 等方法對應於上述故事中的 科室 部分,用於執行具體的操做。常見的 object 科室是將 plan object 類型數據轉換成可觀察值,map 科室是將 map 類型數據轉換成可觀察值....

咱們查閱 observable.object(object) 源碼,其實就 2 行有效代碼:

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

能夠說 observable.object(object) 其實是 extendObservable({}, object) 的別名,從這裏 extendObservable 方法的第一個參數是 {} 能夠看到,最終產生的觀察值對象是基於全新的對象,不影響原始傳入的對象內容

5.一、extendObservable 方法

講到這裏,會有一種恍然大悟,原來 extendObservable 方法纔是最終大 boss,一切觀察值的建立終歸走到這個函數。查看該方法的 源碼,函數簽名以下:

extendObservable(target, properties, decorators, options)
  • 必須接收 2 ~ 4 個參數
  • 第一個參數必須是對象,好比 bankUser
  • 第二個參數是屬性名,好比 name
  • 第三個參數是 裝飾器 配置項,這一知識點在上一篇章已經講解。
  • 第四個參數是配置選項對象

方法具體的使用說明參考 官方文檔 extendObservable

deco

將該方法的主幹找出來:

function extendObservable(target, properties, decorators, options) {
  ...
  
  // 第一步 調用 asObservableObject 方法給 target 添加 $mobx 屬性
  options = asCreateObservableOptions(options);
  var defaultDecorator =
    options.defaultDecorator ||
    (options.deep === false ? refDecorator : deepDecorator);
  asObservableObject(
    target,
    options.name,
    defaultDecorator.enhancer
  ); 
  
  // 第二步 循環遍歷,將屬性通過 decorator(裝飾器) 改造後添加到 target 上
  startBatch();
  for (var key in properties) {
    var descriptor = Object.getOwnPropertyDescriptor(
      properties,
      key
    );
    var decorator =
      decorators && key in decorators
        ? decorators[key]
        : descriptor.get
          ? computedDecorator
          : defaultDecorator;
    var resultDescriptor = decorator(
      target,
      key,
      descriptor,
      true
    );
    if (resultDescriptor){
      Object.defineProperty(target, key, resultDescriptor);
    }
  }
  endBatch();
  return target;

這方法看上去塊頭很大,不過度析起來就 2 大步:

  • 首先調用 asObservableObject 方法,給 target 生成 $mobx 屬性
  • 其次挨個讓每一個屬性通過 decorator 改造後從新安裝到 target 上,默認的 decorator 是 deepDecorator,裝飾器的含義和做用在上一篇文章已講過,點擊 這裏 複習

5.二、第一步:調用 asObservableObject

asObservableObject 方法,主要是給目標對象生成 $mobx 屬性;該 $mobx 屬性對應上述故事中的 科長 角色,用於管理對象的讀寫操做。

爲何要添加 $mobx 屬性?其具體做用又是什麼?

經過閱讀源碼,我無從獲知做者添加 $mobx 屬性的理由,但能夠知道 $mobx 的做用是什麼。

首先,$mobx 屬性是一個 ObservableObjectAdministration 對象,類圖以下:
class

用例子來看看 $mobx 屬性:

var bankUser = observable({
    income: 3,
    name: '張三'
});

console.table(bankUser);

下圖紅框處標示出來的就是 bankUser.$mobx 屬性:
$mobx 屬性

咱們進一步經過如下兩行代碼輸出 $mobx 屬性中具體的數據成員和擁有的方法成員:

console.log(`bankUser.$mobx:`, bankUser.$mobx);
console.log(`bankUser.$mobx.__proto__:`, bankUser.$mobx.__proto__);

$mobx

在這麼多屬性中,格外須要注意的是 writeread 這兩個方法,這兩個方法算是 $mobx 屬性的靈魂,下面即將會講到,這裏先點名一下。

除此以外還須要關注 $mobx 對象中的 values 屬性,剛初始化的時候該屬性是 {} 空對象,不過注意上面截圖中看到 $mobx.values 是有內容的,這其實不是在這一步完成,而是在接下來要講的第二步中所造成的。

你能夠這麼理解,這一步僅僅是找到擔任科長的人選,仍是光桿司令;下一步纔是正式委派科長到某個科室,那個時候新上任的科長才有權力管束其下屬的觀察員。

5.三、第二步:每一個屬性都通過一遍 decorator 的 「洗禮」

這部分就是應用 裝飾器 操做了,默認是使用 deepDecorator 這個裝飾器。裝飾器的應用流程在 上一篇文章 中有詳細講解,直接拿結論過來:

flow

你會發現應用裝飾器的最後一步是在調用 defineObservableProperty 方法時建立 ObservableValue 屬性,對應在 defineObservableProperty 源碼 中如下語句:

var observable = (adm.values[propName] = new ObservableValue(
  newValue,
  enhancer,
  adm.name + '.' + propName,
  false
));

這裏的 adm 就是 $mobx 屬性,這樣新生成的 ObservableValue 實例就掛載在 $mobx.values[propName] 屬性下。

這樣的設定很巧妙,值得咱們深挖。先看一下下面的示例:

var user = {
  income: 3,
  name: '張三'
};
var bankUser = observable(user);

bankUser.income = 5;

console.log(bankUser.income);
console.table(bankUser.$mobx.values.income);

在這個案例中,咱們直接修改 bankUserincome 屬性爲 5,一旦修改,此時 bankUser.$mobx.values.income 也會同步修改:
values

這是怎麼作到的呢?

答案是:經過 generateObservablePropConfig 方法

function generateObservablePropConfig(propName) {
  return (
    observablePropertyConfigs[propName] ||
    (observablePropertyConfigs[propName] = {
      configurable: true,
      enumerable: true,
      get: function() {
        return this.$mobx.read(this, propName);
      },
      set: function(v) {
        this.$mobx.write(this, propName, v);
      }
    })
  );
}

該方法是做用在 decorator 裝飾器其做用期間,用 generateObservablePropConfig 生成的描述符重寫原始對象的描述符,仔細看描述符裏的 getset 方法,對象屬性的 讀寫分別映射到 $mobx.read$mobx.write這兩個方法中

在這裏,咱們就能知道掛載 $mobx 屬性的意圖:MobX 爲咱們建立了原對象屬性的 鏡像 操做,全部針對原有屬性的讀寫操做都將鏡像復刻到 $mobx.values 對應 Observable 實例對象上,從而將複雜的操做隱藏起來,給用戶提供直觀簡單的,提升用戶體驗

以賦值語句 bankUser.income = 5 爲例,這樣的賦值語句咱們平時常常寫,只不過這裏的 bankUser 是咱們 observable.object 操做獲得的,因此 MobX 會同步修改 bankUser.$mobx.values.income 這個 ObservableValue 實例對象,從而觸發 reportChanged 或者 reportObserved 等方法,開啓 響應式鏈 的第一步。

你所作的操做和以往同樣,書寫 bankUser.income = 5 這樣的語句就能夠。而實際上 mobx 在背後默默地作了不少工做,這樣就將簡單的操做留給用戶,而把絕大多數複雜的處理都隱藏給 MobX 框架來處理了。

5.四、遞歸實現觀察值

本小節開始已經說起過遞歸傳遞觀察值,這裏再從代碼層面看一下 遞歸實現觀察值 的原理。這一步是在 decorator 裝飾器應用過程當中,經過 $mobx 掛載對應屬性的 ObservableValue 實例達到的。

對應的操做在剛纔的 5.3 已經講過,仍是在 defineObservableProperty 源碼 那行代碼:

var observable = (adm.values[propName] = new ObservableValue(
  newValue,
  enhancer,
  adm.name + '.' + propName,
  false
));

如下述的 parent 對象爲例:

var parent = {
  child: {
    name: 'tony'
  }
}

當咱們執行 observable(parent)(或者 new ObservableValue(parent)observable.box(parent) 等建立觀察值的方法),其執行路徑以下:

step

從上圖就能夠看到,在 decorator 那一步將屬性轉換成 ObservableValue 實例,這樣在總體上看就是遞歸完成了觀察值的轉換 —— 把 child 和它下屬的屬性也轉換成可觀察值。

六、小測試

請分析 observable.mapobservable.array 的源碼,看看它們和 observable.object 方法之間的差異在哪兒。

七、總結

本文重點是講 Observable 類,與之相關的類圖整理以下:

class

  • ObservableValue 繼承自 Atom,並實現一系列的 接口
  • ObservableObjectAdministration鏡像操做管理者,它主要經過 $mobx 屬性來操控管理每一個觀察值 ObservableValue
  • 比較重要的方法是 interceptobserve ,用「面向切口」編程的術語來說,這兩個方法就是兩個 切口,分別做用於數值更改先後,方便針對數據狀態作一系列的響應;

本文中出現不少 observable 相關的單詞,稍做總結:

  • ObservableValue 是一個普通的 class,用於表示 觀察值 這個概念。
  • observable 是一個函數,也是 mobx 提供的 API,等於 createObservable,表明操做,該操做過程當中會根據狀況調用 observable.object(或者 observable.arrayobservable.map)等方法,最終目的是爲了建立 ObservableValue 對象。
  • extendObservable,這是一個工具函數,算是比較底層的方法,該方法用來向已存在的目標對象添加 observable 屬性;上述的 createObservable 方法其實也是借用該方法實現的;

MobX 默認會遞歸將對象轉換成可觀察屬性,這主要是得益於 enhancer 在其中發揮的做用,由於每一次 Observable 構造函數會對傳入的值通過 enhancer 處理;

有人不由會問,既然提供 observable 方法了,那麼 observable.box 方法存在的意義是什麼?答案是,因爲它直接返回的是 ObservableValue,它相比普通的 observable 建立的觀察值,提供更加細粒度(底層)的操做;

好比它除了能像正常觀察值同樣和 autorun 搭配使用以外,建立的對象還直接擁有 interceptobserve 方法:

var pr1 = observable.box(2);
autorun(() => {
  console.log('value:', pr1.get());
});
pr1.observe(change => {
  console.log('change from', change.oldValue, 'to', change.newValue);
});

pr1.set(3);

// 如下是輸出結果:
// value: 2
// value: 3
// change from 2 to 3

固然 MobX 考慮也很周全,還單獨提供 Intercept & Observe 兩個工具函數,以函數調用的方式給觀察值新增這兩種回調函數。

所以下述兩種方式是等同的,能夠本身試驗一下:

// 調用 observe 屬性方法
pr1.observe(change => {
  console.log('change from', change.oldValue, 'to', change.newValue);
});

// 使用 observe 工具函數能夠達到相同的目的
observe(pr1, change => {
    console.log('change from', change.oldValue, 'to', change.newValue);
}):


本文針對 MobX 4 源碼講解,而在 MobX 5 版本中的 Observable 類則是採用 proxy 來實現 Observable,總體思路和上述的並沒有二致,只是在細節方面將 Object.defineProperty 替換成 new Proxy 的寫法而已,感興趣的同窗建議先閱讀 《抱歉,學會 Proxy 真的能夠隨心所欲》瞭解 Proxy 的寫法,而後去看一下 MobX 5 中的 observable.object 方法已經改用 createDynamicObservableObject 來建立 proxy,所建立的 proxy 模型來自於 objectProxyTraps 方法;若有機會將在後續的文章中更新這方面的知識。

用故事講解 MobX 源碼的系列文章至此告一段落,後續以散篇的形式發佈跟 MobX 相關的文章。

下面的是個人公衆號二維碼圖片,歡迎關注,及時獲取最新技術文章。
微信公衆號

相關文章
相關標籤/搜索