成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

leeon / 2110人閱讀

摘要:前言初衷以系列故事的方式展現(xiàn)源碼邏輯,盡可能以易懂的方式講解源碼本系列文章用故事解讀源碼一用故事解讀源碼二用故事解讀源碼三用故事解讀源碼四裝飾器和用故事解讀源碼五文章編排每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解所涉及人物場

================前言===================

初衷:以系列故事的方式展現(xiàn) MobX 源碼邏輯,盡可能以易懂的方式講解源碼;

本系列文章

《【用故事解讀 MobX源碼(一)】 autorun》

《【用故事解讀 MobX源碼(二)】 computed》

《【用故事解讀 MobX源碼(三)】 shouldCompute》

《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》

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

文章編排:每篇文章分成兩大段,第一大段以簡單的偵探系列故事的形式講解(所涉及人物、場景都以 MobX 中的概念為原型創(chuàng)建),第二大段則是源碼講解。

本文基于 MobX 4 源碼講解

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

A. Story Time

最高警長看完執(zhí)行官(MobX)的自動部署方案,對 “觀察員” 這個基層人員工作比較感興趣,自執(zhí)行官拿給他部署方案的時候,他就注意到所有上層人員的功能都是基于該底層人員高效的工作機制;

第二天,他找上執(zhí)行官(MobX)一起去視察“觀察員”所在機構(gòu)部門(下面簡稱為 ”觀察局“),想更深入地了解 “觀察員” 運行分配機制。

當(dāng)最高警長到達(dá)部門的時候,恰好遇到該部門恰好要開始執(zhí)行 MobX 前不久新下發(fā)的任務(wù),要求監(jiān)控 parent 對象的一舉一動:

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

var bankUser = observable(parent);

任務(wù)達(dá)到觀察局辦公室后,相應(yīng)的辦公室文員會對任務(wù)進(jìn)行分析,然后會依據(jù)對象類型交給相應(yīng)科室進(jìn)行處理,常見的有 object 科,另外還有 map 科和 array 科;

現(xiàn)在,辦公室文員見傳入的對象是 parent 是個對象,就將其傳遞給 object 科,讓其組織起一起針對該 parent 對象的 ”觀察小組“,組名為 bankUser

object 科接到任務(wù),委派某位科長(以下稱為 bankUser 科長)組成專項負(fù)責(zé)此 parent 對象的觀察工作,bankUser 科長接手任務(wù)后發(fā)現(xiàn)有兩個屬性,其中 child 是對象類型,age 是原始值類型,就分別將任務(wù)委派給 child 小科長 和 name 觀察員 O1,child 小科長接到任務(wù)后再委派給 name 觀察員 O2 和 age 觀察員 O3,最終執(zhí)行該任務(wù)的人員結(jié)構(gòu)如下:

觀察員的任務(wù)職責(zé)我們已經(jīng)很熟悉了,當(dāng)讀寫觀察員對應(yīng)的數(shù)據(jù)時將觸發(fā) reportObservedpropagateChanged 方法;

這里涉及到兩位科長(bankUser 科長 和 child 小科長),那么科長的任務(wù)職責(zé)是什么呢?

科長的人物職責(zé)是起到 管理 作用,它負(fù)責(zé)統(tǒng)管在他名下的觀察員。比如當(dāng)我們讀寫 bankUser.child 對象的 name 屬性時(比如執(zhí)行語句 bankUser.child.name = "Jack"),首先感知到讀寫操作的并非是 觀察員 O2 而是bankUser科長,bankUser科長會告知 child 小科長有數(shù)據(jù)變更,child 小科長然后再將信息傳達(dá)給 name 觀察員 O2 ,然后才是觀察員 O2 對數(shù)據(jù)讀寫起反應(yīng),這才讓觀察員 O2 發(fā)揮作用。

從代碼層面看,我們看到僅僅是執(zhí)行 bankUser.child.name = "Jack"這一行語句,和我們平常修改對象屬性并無二致。然而在這一行代碼背后其實牽動了一系列的操作。這其實是 MobX 構(gòu)建起的一套 ”鏡像“ 系統(tǒng),使用者仍舊按平時的方式讀寫對象,然而每個屬性的讀寫操作實則都鏡像到觀察局 的某個小組具體的操作;非常類似于古代的 ”垂簾聽政“ ,看似皇帝坐在文武百官前面,其實真正做出決策響應(yīng)的是藏在簾后面的那個人。

前幾章中我們只看到觀察員在活動,然則背后離不開 科長 這一角色機制在背后暗暗的調(diào)度。對每項任務(wù),最終都會落實到觀察員采取“一對一”模式監(jiān)控分配到給自己的觀察項,而每個觀察員肯定是隸屬于某個 ”科長“ 帶領(lǐng)。在 MobX 系統(tǒng)里,辦公室、科長和觀察員是密不可分,共同構(gòu)建起 觀察局 運行體制;

"分工明確,運轉(zhuǎn)高效",這是最高警長在巡視完觀察員培訓(xùn)基地后的第一印象,觀察局運轉(zhuǎn)的每一步的設(shè)計都有精細(xì)的考量;

B. Source Code Time

先羅列本文故事中人物與 MobX 源碼概念映射關(guān)系:

故事人物 MobX 源碼 解釋
警署最高長官 (無) MobX 用戶,沒錯,就是你
執(zhí)行官 MobX MobX 整個 MobX 運行環(huán)境
觀察局辦公室(主任、文員) observable、observable.box 等 用于創(chuàng)建 Observable 的 API
object 科室、map 科室、array 科室 observable.object、observable.map、observable.array 將不同復(fù)合類型轉(zhuǎn)換成觀察值的方法
科長 ObservableObjectAdministration 主要給對象添加 $mobx 屬性
觀察員 ObservableValue 實例 ObservableValue 實例
1、總?cè)肟冢簅bservable

observable 對應(yīng)上述故事中的 觀察局辦公室主任 角色,本身不提供轉(zhuǎn)換功能,主要是起到統(tǒng)一調(diào)度作用 —— 這樣 MobX 執(zhí)行官只需要將命令發(fā)給辦公室人員就行,至于內(nèi)部具體的操作、具體由哪個科室處理,MobX 執(zhí)行官不需要關(guān)心。

將與 observable 的源碼 相關(guān)的源碼稍微整理,就是如下的形式:

var observable = createObservable;
// 使用“奇怪”的方式來擴展 observable 函數(shù)的功能,就是將 observableFactories 的方法挨個拷貝給 observable
Object.keys(observableFactories).forEach(function(name) {
  return (observable[name] = observableFactories[name]);
});

首先 observable 是函數(shù),函數(shù)內(nèi)容就是 createObservable

其次 observable 是對象,對象屬性和 observableFactories 一致

也就是說 observable 其實是 各種構(gòu)造器的總和,整合了 createObservable(默認(rèn)構(gòu)造器) + observableFactories(其他構(gòu)造器)

自己也可以在 console 控制臺中打印來驗證一番:

const { observable } = mobx;

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

從以下控制臺輸出的結(jié)果來看,observable 的屬性的確來自于createObservableobservableFactories 這兩者:

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

這里我大致劃分了一下,分成 4 部分內(nèi)容來理解:

第一部分:createObservable 方法剛才粗略講過,是 MobX API 的 observable 的別名,是一個高度封裝的方法,算是一個總?cè)肟冢奖阌脩粽{(diào)用;該部分對應(yīng)上述故事中的 觀察局辦公室主任 的角色

第二部分:box 是一個轉(zhuǎn)換函數(shù),用于將 原值(primitive value) 直接轉(zhuǎn)換成 ObservableValue 對象;shallowBoxbox 函數(shù)的非 deep 版本;該部分對應(yīng)上述故事中的 觀察局辦公室文員 的角色;

第三部分:針對 object、array 以及 map 這三種數(shù)據(jù)類型分別提供轉(zhuǎn)換函數(shù),同時也提供 shallow 的版本;該部分對應(yīng)上述故事中的 科室 部分;

第四部分:提供四種裝飾器函數(shù),裝飾器的概念我們上一節(jié)課講過,主要輔助提供裝飾器語法糖作用;對普通 MobX 用戶來講這部分平時也是接觸不到的;

如何理解這 4 部分的之前的關(guān)系呢?我個人的理解如下:

第三部分屬于 “基層建筑”,分別為 object、array 以及 map 這三種數(shù)據(jù)類型提供轉(zhuǎn)換成可觀察值的功能(默認(rèn)是遞歸轉(zhuǎn)換,shallow 表示非遞歸轉(zhuǎn)換);這部分對應(yīng)上述故事中的科室概念,不同的觀察任務(wù)由不同的科室來處理;

第一部分和第二部分屬于 “上層建筑”,提供統(tǒng)一的接口,具體的轉(zhuǎn)換功能都是調(diào)用第三部分中的某個轉(zhuǎn)換函數(shù)來實現(xiàn)的;這兩部分對應(yīng)上述故事中的 觀察局辦公室 部分。

第一部分我們最熟悉,不過第二部分的 box 函數(shù)轉(zhuǎn)換能力反而比第一部分更廣,支持將原始值轉(zhuǎn)換成可觀察值

第四部分和另外三部分沒有直接的關(guān)系,主要輔助提供裝飾器函數(shù);注意,沒有直接的聯(lián)系并不代表沒有聯(lián)系,第四部分中裝飾器內(nèi)的核心邏輯和另外三部分是一樣的(比如都調(diào)用 decorator 方法)。

下面我們看兩個具體的示例,來輔助消化上面的結(jié)論。

示例一observable.box(obj) 底層就是調(diào)用 observable.object(obj)實現(xiàn)的

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

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


可以發(fā)現(xiàn) bankUser2 中的 value 屬性部分內(nèi)容和 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 方法實現(xiàn)原始值轉(zhuǎn)換:

2、第一部分:createObservable

正如上面所言,該函數(shù)其實就是 MobX API 的 observable 的 “別名”。所以也是對應(yīng)上述故事中的 觀察局辦公室主任 角色;

該函數(shù)本身不提供轉(zhuǎn)換功能,只是起到 "轉(zhuǎn)發(fā)" 作用,將傳入的對象轉(zhuǎn)發(fā)給對應(yīng)具體的轉(zhuǎn)換函數(shù)就行了;

看一下 源碼,

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)"`
    )
}

不難看出其實是典型的采用了 策略設(shè)計模式 ,將多種數(shù)據(jù)類型(Object、Array、Map)情況的轉(zhuǎn)換封裝起來,好讓調(diào)用者不需要關(guān)心實現(xiàn)細(xì)節(jié):

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

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

走向 ① 是 裝飾器語法所特有的,這是因為此時傳給 createObservable 的第二個參數(shù)是 string 類型,這一點我們在上一篇文章有詳細(xì)論述;

走向 ② 很直觀,如果傳入的參數(shù)就已經(jīng)是 觀察值 了,不多廢話直接返回傳入的值,不需要轉(zhuǎn)換;

走向 ③、④ 、⑤ 是直根據(jù)傳入?yún)?shù)的類型分別調(diào)用具針對具體類型的轉(zhuǎn)換方法;

走向 ⑥,在上面示例中我們已經(jīng)看到過, 針對原始值會提示建議用戶使用 observable.box 方法。

第一部分的 createObservable 的內(nèi)容就那么些,總之只是起了 “向?qū)А?作用。是不是比你想象中的要簡單?

接下來我們繼續(xù)看第二部分的 observable.box 方法。

3、第二部分:observable.box

這個方法對應(yīng)上述故事中的 觀察局辦公室文員 角色,也是屬于辦公室部門的,所起到的作用和 主任 大同小異,只是平時我們用得并不多罷了。

當(dāng)我第一次閱讀 官網(wǎng)文檔 中針對有關(guān) observable.box 的描述時:

來回讀了幾次,“盒子”是個啥?它干嘛用的? “observable” 和 “盒子” 有半毛錢關(guān)系?

直到看完該函數(shù)的詳細(xì)介紹 boxed values 后,方才有所感悟,這里這 box 方法就是將普通函數(shù) “包裝” 成可觀察值,所以 box 是動詞而非名詞

準(zhǔn)確地理解,observable.box 是一個轉(zhuǎn)換函數(shù),比如我們將普通的原始值 "Pekin"(北京)轉(zhuǎn)換成可觀察值,就可以使用:

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

原始值 "Pekin" 并不具備可觀察屬性,而經(jīng)過 box 方法操作之后的 cityName 變量具有可觀察性,比如:

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

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

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

從輸入輸出角度來看,這 box 其實就是將普通對象轉(zhuǎn)換成可觀察值的過程,轉(zhuǎn)換過程中將一系列能力“添加”到對象上,從而獲得 “自動響應(yīng)數(shù)值變化” 的能力。

那么具體這 box 函數(shù)是如何實現(xiàn)的呢?直接看 源碼。

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

發(fā)現(xiàn)該方法僅僅是調(diào)用 ObservableValue 構(gòu)造函數(shù),所以 box 方法操作的結(jié)果是返回 ObservableValue 實例。

這里的 asCreateObservableOptions 方法僅僅是格式化入?yún)?options 對象而已。
4、核心類:ObservableValue

總算是講到這個 ObservableValue 類了,該類是理解可觀察值的關(guān)鍵概念。這個類對應(yīng)上述故事中的 觀察員 角色,就是最基層的 name 觀察員 O1、O2、O3 那些。

本篇文章的最終目的也就是為了講清楚這個 ObservableValue 類,其他的概念反而是圍繞它而創(chuàng)建起來的。

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

你會發(fā)現(xiàn)該類 繼承自 Atom 類,所以在理解 ObservableValue 之前必須理解 Atom。

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

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

當(dāng)它的值被使用的時候,就會觸發(fā) reportObserved 方法,在 第一篇文章 的講解中可知,MobX 正是基于該方法,使得觀察員和探長之間建立關(guān)聯(lián)關(guān)系。

當(dāng)它的值受到更改的時候,將會觸發(fā) reportChanged 方法,在第三篇文章 《【用故事解讀 MobX源碼(三)】 shouldCompute》中可知,基于該方法觀察員就可以將 非穩(wěn)態(tài)信息逐層上傳,最終將讓探長、會計員重新執(zhí)行任務(wù)。

Atom 類圖如下,從中我們看到前面幾章中所涉及到的 onBecomeUnobserved、onBecomeObservedreportObserved、reportChanged 這幾個核心方法,它們都來源于 Atom 這個類:

所以說 Atom 是整個 MobX 的基石并不為過,所有的自動化響應(yīng)機制都是建立在這個最最基礎(chǔ)類之上。正如在大自然中,萬物都是由原子(atom)構(gòu)成的,借此意義, MobX 中的 ”具備響應(yīng)式的“ 對象都是由這個 Atom 類構(gòu)成的。
ComputeValue類 也繼承自 Atom,Reaction 類的實現(xiàn)得依靠 Atom,因此不難感知 Atom 基礎(chǔ)重要性)

4.2、createAtom

理論上你只要創(chuàng)建一個 Atom 實例就能融入到 mobx 的響應(yīng)式系統(tǒng)中,

如何自己創(chuàng)建一個 Atom 呢?

MobX 已經(jīng)暴露了一個名為 createAtom 方法,
官方文檔 創(chuàng)建 observable 數(shù)據(jù)結(jié)構(gòu)和 reactions(反應(yīng)) 給出了創(chuàng)建一個 鬧鐘 的例子,具體講解了該 createAtom 方法的使用:

...
  // 創(chuàng)建 atom 就能和 MobX 核心算法交互
  this.atom = createAtom(
      // 第一個參數(shù)是 name 屬性,方便后續(xù) 
      "Clock",
      // 第二個參數(shù)是回調(diào)函數(shù),可選,當(dāng) atom 從 unoberved 狀態(tài)轉(zhuǎn)變到 observed 
      () => this.startTicking(),
      // 第三個參數(shù)也是回調(diào)函數(shù),可選,與第二個參數(shù)對應(yīng),此回調(diào)是當(dāng) atom 從 oberved 狀態(tài)轉(zhuǎn)變到 unobserved 時會被調(diào)用
      // 注意到,同一個 atom 有可能會在 oberved 狀態(tài)和 unobserved 之間多次轉(zhuǎn)換,所以這兩個回調(diào)有可能會多次被調(diào)用
      () => this.stopTicking()
  );
...

同時文中也給出了對應(yīng)的最佳實踐:

最好給創(chuàng)建的 Atom 起一個名字,方便后續(xù) debug

onBecomeObservedonBecomeUnobserved 和我們面向?qū)ο笾袠?gòu)造函數(shù)與析構(gòu)函數(shù)的作用相似,方便進(jìn)行資源的申請和釋放。

不過 Atom 實例這個還是偏向底層實現(xiàn)層,除非需要強自定義的特殊場景中,平時我們推薦直接使用 observable 或者 observable.box 來創(chuàng)建觀察值更為簡單直接;

4.3、理解 ObservableValue

MobX 在 Atom 類基礎(chǔ)上,泛化出一個名為 ObservableValue 類,就是我們耳熟能詳?shù)?觀察值 了。從代碼層面上來看,實現(xiàn) ObservableValue 其實就是繼承一下 Atom 這個類,然后再添加許多輔助的方法和屬性就可以了。

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

4.3.1、Intercept & Observe 部分

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

如果把 “對象變更” 作為事件,那么我們可以在 事件發(fā)生之前事件方法之后 這兩個 “切面” 分別可以安插回調(diào)函數(shù)(callback),方便程序動態(tài)擴展,這屬于 面向切面編程的思想。

不了解 AOP 的,可以查閱 知乎問答-什么是面向切面編程AOP?

在 MobX 世界里,將安插在 事件發(fā)生之前 的回調(diào)函數(shù)稱為 intercept,將安插在 事件發(fā)生之后 的回調(diào)函數(shù)稱為 observe。理解這兩個方法可以去看 官方中的示例,能快速體會其作用。

這里稍微進(jìn)一步講細(xì)致一些,有時候官方文檔會中把 intercept 理解成 攔截器。 這是因為它作用于事件(數(shù)據(jù)變更)發(fā)生之前,因此可以操縱變更的數(shù)據(jù)內(nèi)容,甚至可以通過返回 null 忽略某次數(shù)據(jù)變化而不讓它生效。

其作用機制也很直接,該方法調(diào)用的最終都是調(diào)用實例的 intercept 方法,這樣每次在值變更之前(以下 prepareNewValue 方法執(zhí)行),都會觸發(fā)觀察值上所綁定的所有的 攔截器

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 安插的回調(diào)中返回 null 的話,相當(dāng)于告知 MobX 數(shù)值沒有變更(UNCHANGED),既然值沒有變更,后續(xù)的邏輯就不會觸發(fā)了。

observe 的作用是將回調(diào)函數(shù)安插在值變更之后(以下 setNewValue 方法調(diào)用),同樣是通過調(diào)用 notifyListeners 通知所有的監(jiān)聽器

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

==========【以下是額外的知識內(nèi)容,可跳過,不影響主線講解】===========

如何解除安插的回調(diào)函數(shù)?

Intercept & Observe 這兩個函數(shù)返回一個 disposer 函數(shù),這個函數(shù)是 解綁函數(shù),調(diào)用該函數(shù)就可以取消攔截器或者監(jiān)聽器 了。這里有一個最佳實踐,如果不需要某個攔截器或者監(jiān)聽器了,記得要及時清理自己綁定的監(jiān)聽函數(shù) 永遠(yuǎn)要清理 reaction —— 即調(diào)用 disposer 函數(shù)。

那么如何實現(xiàn) disposer 解綁函數(shù)這套機制?

以攔截器(intercept)為例,注冊的時候調(diào)用 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 數(shù)組屬性中。關(guān)鍵是在于返回值,返回的是一個閉包 —— once 函數(shù)調(diào)用的結(jié)果值。

所以我們簡化一下 disposer 解綁函數(shù)的定義:

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

恰是這個 once 函數(shù)是實現(xiàn)解綁功能的核心

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

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

once 方法其實通過 invoked 變量,控制傳入的 func 函數(shù)只調(diào)用一次。

回過頭來 disposer 解綁函數(shù),調(diào)用一次就會從 interceptors 數(shù)組中移除當(dāng)前攔截器。使用 once 函數(shù)后,你無論調(diào)用多少次 disposer 方法,最終都只會解綁一次。

由于 once 是純函數(shù),因此大伙兒可以提取出來運用到自己的代碼庫中 —— 這也是源碼閱讀的益處之一,借鑒源碼中優(yōu)秀部分,然后學(xué)習(xí)吸收,引以為用。

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

4.3.2、enhancer 部分

這部分是在 ObservableValue 構(gòu)造函數(shù)中發(fā)揮作用的,其影響的恰恰是最核心的數(shù)據(jù)屬性:

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

在上一篇文章《【用故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer》中有提及過 enhance,在那里我們說起過 enhance 其實就是裝飾器(decorator)的有效成分,該有效成分影響的正是本節(jié)所講的 ObservableValue 對象。結(jié)合 types/modifier.ts 中有各種 Enhancer 的具體內(nèi)容,就能大致了解 enhancer 是如何起到 轉(zhuǎn)換數(shù)值 的作用的,以常見的 deepEnhancer 為例,當(dāng)在構(gòu)造函數(shù)中執(zhí)行 _this.value = enhancer(value, undefined, name); 的時候會進(jìn)入到 deepEnhance 函數(shù)體內(nèi):

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;
}

這段代碼是否似曾相識?!沒錯,和上一節(jié)所述 createObservable 方法幾乎一樣,采用 策略設(shè)計模式 調(diào)用不同具體轉(zhuǎn)換函數(shù)(比如 observable.object 等)。

現(xiàn)在應(yīng)該能夠明白,第一部分的 createObservable 和 第二部分的 observable.box 都是建立在第三部分之上,而且通過第一部分、第二部分以及第三部分獲得的觀察值對象都是屬于觀察值對象(ObservableValue),大同小異,頂多只是“外形”有略微的差別。

通過該 enhancer 部分的講解,我們發(fā)現(xiàn)所有待分析的重要部分都聚焦到第三部分的 observable.object 等這些個轉(zhuǎn)換方法身上了。

5、第三部分:observable.object

因為結(jié)構(gòu)的原因,上面先講了最基層的 ObservableValue 部分,現(xiàn)在回來講的 observable.object 方法。從這里你能大概體會到 MobX 體系中遞歸現(xiàn)象new ObservableValue 里面會調(diào)用 observable.object 方法,從后面的講解里你將會看到 observable.object 方法里面也會調(diào)用 new ObservableValue 的操作,所以 遞歸地將對象轉(zhuǎn)換成可觀察值 就很順理成章。

閱讀官方文檔 Observable.object,該 observable.object 方法就是把一個普通的 JavaScript 對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉(zhuǎn)變成可觀察的,而且 observable 是 遞歸應(yīng)用 的。

observable.object 等方法對應(yīng)于上述故事中的 科室 部分,用于執(zhí)行具體的操作。常見的 object 科室是將 plan object 類型數(shù)據(jù)轉(zhuǎn)換成可觀察值,map 科室是將 map 類型數(shù)據(jù)轉(zhuǎn)換成可觀察值....

我們查閱 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 方法的第一個參數(shù)是 {} 可以看到,最終產(chǎn)生的觀察值對象是基于全新的對象,不影響原始傳入的對象內(nèi)容。

5.1、extendObservable 方法

講到這里,會有一種恍然大悟,原來 extendObservable 方法才是最終大 boss,一切觀察值的創(chuàng)建終歸走到這個函數(shù)。查看該方法的 源碼,函數(shù)簽名如下:

extendObservable(target, properties, decorators, options)

必須接收 2 ~ 4 個參數(shù)

第一個參數(shù)必須是對象,比如 bankUser

第二個參數(shù)是屬性名,比如 name

第三個參數(shù)是 裝飾器 配置項,這一知識點在上一篇章已經(jīng)講解。

第四個參數(shù)是配置選項對象

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

將該方法的主干找出來:

function extendObservable(target, properties, decorators, options) {
  ...
  
  // 第一步 調(diào)用 asObservableObject 方法給 target 添加 $mobx 屬性
  options = asCreateObservableOptions(options);
  var defaultDecorator =
    options.defaultDecorator ||
    (options.deep === false ? refDecorator : deepDecorator);
  asObservableObject(
    target,
    options.name,
    defaultDecorator.enhancer
  ); 
  
  // 第二步 循環(huán)遍歷,將屬性經(jīng)過 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 大步:

首先調(diào)用 asObservableObject 方法,給 target 生成 $mobx 屬性

其次挨個讓每個屬性經(jīng)過 decorator 改造后重新安裝到 target 上,默認(rèn)的 decorator 是 deepDecorator,裝飾器的含義和作用在上一篇文章已講過,點擊 這里 復(fù)習(xí)

5.2、第一步:調(diào)用 asObservableObject

asObservableObject 方法,主要是給目標(biāo)對象生成 $mobx 屬性;該 $mobx 屬性對應(yīng)上述故事中的 科長 角色,用于管理對象的讀寫操作。

為什么要添加 $mobx 屬性?其具體作用又是什么?

通過閱讀源碼,我無從獲知作者添加 $mobx 屬性的理由,但可以知道 $mobx 的作用是什么。

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

用例子來看看 $mobx 屬性:

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

console.table(bankUser);

下圖紅框處標(biāo)示出來的就是 bankUser.$mobx 屬性:

我們進(jìn)一步通過以下兩行代碼輸出 $mobx 屬性中具體的數(shù)據(jù)成員和擁有的方法成員:

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

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

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

你可以這么理解,這一步僅僅是找到擔(dān)任科長的人選,還是光桿司令;下一步才是正式委派科長到某個科室,那個時候新上任的科長才有權(quán)力管束其下屬的觀察員。

5.3、第二步:每個屬性都經(jīng)過一遍 decorator 的 “洗禮”

這部分就是應(yīng)用 裝飾器 操作了,默認(rèn)是使用 deepDecorator 這個裝飾器。裝飾器的應(yīng)用流程在 上一篇文章 中有詳細(xì)講解,直接拿結(jié)論過來:

你會發(fā)現(xiàn)應(yīng)用裝飾器的最后一步是在調(diào)用 defineObservableProperty 方法時創(chuàng)建 ObservableValue 屬性,對應(yīng)在 defineObservableProperty 源碼 中以下語句:

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

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

這樣的設(shè)定很巧妙,值得我們深挖。先看一下下面的示例:

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 也會同步修改:

這是怎么做到的呢?

答案是:通過 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 生成的描述符重寫原始對象的描述符,仔細(xì)看描述符里的 getset 方法,對象屬性的 讀寫分別映射到 $mobx.read$mobx.write這兩個方法中。

在這里,我們就能知道掛載 $mobx 屬性的意圖:MobX 為我們創(chuàng)建了原對象屬性的 鏡像 操作,所有針對原有屬性的讀寫操作都將鏡像復(fù)刻到 $mobx.values 對應(yīng) Observable 實例對象上,從而將復(fù)雜的操作隱藏起來,給用戶提供直觀簡單的,提高用戶體驗。

以賦值語句 bankUser.income = 5 為例,這樣的賦值語句我們平時經(jīng)常寫,只不過這里的 bankUser 是我們 observable.object 操作得到的,所以 MobX 會同步修改 bankUser.$mobx.values.income 這個 ObservableValue 實例對象,從而觸發(fā) reportChanged 或者 reportObserved 等方法,開啟 響應(yīng)式鏈 的第一步。

你所做的操作和以往一樣,書寫 bankUser.income = 5 這樣的語句就可以。而實際上 mobx 在背后默默地做了很多工作,這樣就將簡單的操作留給用戶,而把絕大多數(shù)復(fù)雜的處理都隱藏給 MobX 框架來處理了。

5.4、遞歸實現(xiàn)觀察值

本小節(jié)開始已經(jīng)提及過遞歸傳遞觀察值,這里再從代碼層面看一下 遞歸實現(xiàn)觀察值 的原理。這一步是在 decorator 裝飾器應(yīng)用過程中,通過 $mobx 掛載對應(yīng)屬性的 ObservableValue 實例達(dá)到的。

對應(yīng)的操作在剛才的 5.3 已經(jīng)講過,還是在 defineObservableProperty 源碼 那行代碼:

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

以下述的 parent 對象為例:

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

當(dāng)我們執(zhí)行 observable(parent)(或者 new ObservableValue(parent) 、 observable.box(parent) 等創(chuàng)建觀察值的方法),其執(zhí)行路徑如下:

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

6、小測試

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

7、總結(jié)

本文重點是講 Observable 類,與之相關(guān)的類圖整理如下:

ObservableValue 繼承自 Atom,并實現(xiàn)一系列的 接口;

ObservableObjectAdministration鏡像操作管理者,它主要通過 $mobx 屬性來操控管理每個觀察值 ObservableValue

比較重要的方法是 interceptobserve ,用“面向切口”編程的術(shù)語來講,這兩個方法就是兩個 切口,分別作用于數(shù)值更改前后,方便針對數(shù)據(jù)狀態(tài)做一系列的響應(yīng);

本文中出現(xiàn)很多 observable 相關(guān)的單詞,稍作總結(jié):

ObservableValue 是一個普通的 class,用于表示 觀察值 這個概念。

observable 是一個函數(shù),也是 mobx 提供的 API,等于 createObservable,代表操作,該操作過程中會根據(jù)情況調(diào)用 observable.object(或者 observable.arrayobservable.map)等方法,最終目的是為了創(chuàng)建 ObservableValue 對象。

extendObservable,這是一個工具函數(shù),算是比較底層的方法,該方法用來向已存在的目標(biāo)對象添加 observable 屬性;上述的 createObservable 方法其實也是借用該方法實現(xiàn)的;

MobX 默認(rèn)會遞歸將對象轉(zhuǎn)換成可觀察屬性,這主要是得益于 enhancer 在其中發(fā)揮的作用,因為每一次 Observable 構(gòu)造函數(shù)會對傳入的值經(jīng)過 enhancer 處理;

有人不禁會問,既然提供 observable 方法了,那么 observable.box 方法存在的意義是什么?答案是,由于它直接返回的是 ObservableValue,它相比普通的 observable 創(chuàng)建的觀察值,提供更加細(xì)粒度(底層)的操作;

比如它除了能像正常觀察值一樣和 autorun 搭配使用之外,創(chuàng)建的對象還直接擁有 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);

// 以下是輸出結(jié)果:
// value: 2
// value: 3
// change from 2 to 3

當(dāng)然 MobX 考慮也很周全,還多帶帶提供 Intercept & Observe 兩個工具函數(shù),以函數(shù)調(diào)用的方式給觀察值新增這兩種回調(diào)函數(shù)。

因此下述兩種方式是等同的,可以自己試驗一下:

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

// 使用 observe 工具函數(shù)可以達(dá)到相同的目的
observe(pr1, change => {
    console.log("change from", change.oldValue, "to", change.newValue);
}):


本文針對 MobX 4 源碼講解,而在 MobX 5 版本中的 Observable 類則是采用 proxy 來實現(xiàn) Observable,整體思路和上述的并無二致,只是在細(xì)節(jié)方面將 Object.defineProperty 替換成 new Proxy 的寫法而已,感興趣的同學(xué)建議先閱讀 《抱歉,學(xué)會 Proxy 真的可以為所欲為》了解 Proxy 的寫法,然后去看一下 MobX 5 中的 observable.object 方法已經(jīng)改用 createDynamicObservableObject 來創(chuàng)建 proxy,所創(chuàng)建的 proxy 模型來自于 objectProxyTraps 方法;如有機會將在后續(xù)的文章中更新這方面的知識。

用故事講解 MobX 源碼的系列文章至此告一段落,后續(xù)以散篇的形式發(fā)布跟 MobX 相關(guān)的文章。

下面的是我的公眾號二維碼圖片,歡迎關(guān)注,及時獲取最新技術(shù)文章。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96563.html

相關(guān)文章

  • 故事解讀 MobX 源碼(四)】裝飾器 和 Enhancer

    摘要:所以這是一篇插隊的文章,用于去理解中的裝飾器和概念。因此,該的作用就是根據(jù)入?yún)⒎祷鼐唧w的描述符。其次局部來看,裝飾器具體應(yīng)用表達(dá)式是,其函數(shù)簽名和是一模一樣。等裝飾器語法,是和直接使用是等效等價的。 ================前言=================== 初衷:以系列故事的方式展現(xiàn) MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解...

    maybe_009 評論0 收藏0
  • 故事解讀 MobX源碼(一)】 autorun

    摘要:隨后,執(zhí)行官給出一張當(dāng)張三存款發(fā)生變化之時,此機構(gòu)的運作時序圖的確,小機構(gòu)靠人力運作,大機構(gòu)才靠制度運轉(zhuǎn)。第一條語句創(chuàng)建觀察員第一條語句張三我們調(diào)用的時候,就創(chuàng)建了對象,對象的所有屬性都將被拷貝至一個克隆對象并將克隆對象轉(zhuǎn)變成可觀察的。 ================前言=================== 初衷:網(wǎng)上已有很多關(guān)于 MobX 源碼解讀的文章,但大多閱讀成本甚高。...

    qieangel2013 評論0 收藏0
  • 故事解讀 MobX源碼(三)】 shouldCompute

    摘要:最簡單的情況張三的存貸這里我們創(chuàng)建了實例探長實例觀察員這個示例和我們之前在首篇文章用故事解讀源碼一中所用示例是一致的。 ================前言=================== 初衷:以系列故事的方式展現(xiàn) MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...

    JackJiang 評論0 收藏0
  • 故事解讀 MobX源碼(二)】 computed

    摘要:場景為了多維度掌控嫌疑犯的犯罪特征數(shù)據(jù),你警署最高長官想要獲取并實時監(jiān)控張三的貸款數(shù)額存貸比存款和貸款兩者比率的變化。 ================前言=================== 初衷:以系列故事的方式展現(xiàn) MobX 源碼邏輯,盡可能以易懂的方式講解源碼; 本系列文章: 《【用故事解讀 MobX源碼(一)】 autorun》 《【用故事解讀 MobX源碼(二)】...

    Ethan815 評論0 收藏0
  • Mobx 源碼閱讀簡記

    摘要:源碼簡記整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒主要三大部分的原子類,能夠被觀察和通知變化,繼承于。同時里面有幾個比較重要的屬性與方法。 Mobx 源碼簡記 整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒~ 主要三大部分Atom、Observable、Derivation Atom Mobx的原子類,能夠被觀察和通知變化,obs...

    paulli3 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<