What does mobx react to?

Mobx 是一個通過戰火洗禮的庫,它經過透明的 函數響應式編程(transparently applying functional reactive programming - TFRP)使得 狀態管理變得簡單和可擴展.

Mobx

上面這段話引自 Mobx 的官方文檔,說明了 Mobx 是一個應用了函數響應式的狀態管理庫。所謂的響應式就是事件監聽,也是 Mobx 背後的哲學:javascript

任何源自應用狀態的東西都應該自動地得到

        這裏說的 「應用狀態」 就是 state,在 Mobx 的世界裏叫 observable;源自應用狀態的 「東西」 叫作 derivations,derivations 能夠分爲兩大類:computedreaction
        computed 表示從應用狀態派生出來的新狀態,也就是派生值。好比你定義了兩個 state 分別叫作 a 和 b,它們的和叫作 total,而 total 能夠經過 a + b 獲得,你不必定義一個新的 state,這個 total 就叫作 computed。
        reaction 表示從應用狀態派生出來的反作用,也就是派生行爲。好比有一個分頁選擇器:你用一個叫作 index 的 state 表示當前頁碼,初始值是 1,當你改變這個 index 值爲 2 的時候,就須要觸發一個跳轉到第2頁的行爲,這個行爲是由 index 派生出來的,就叫作 reaction。
        Mobx 的核心概念其實就是這三個:observable、computed 和 reaction。html

依賴收集

任何源自應用狀態的東西都應該自動地得到

        上面這句話還有很重要的一點沒有講到,就是 Mobx 哲學所聲明的 「自動」,用高大上一點的術語講就是依賴收集。咱們能夠舉個栗子:java

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message.title)
})
// 輸出:
// "Foo"

message.title = "Bar"
// 輸出:
// "Bar"

        咱們聲明瞭一個 observable 的 message 對象,並調用了一個 autorun 函數用來輸出 message 的 title 屬性,這時候控制檯立刻輸出 "Foo"。嗯,一切都在掌控之中。
        接下來咱們嘗試修改了 message 的 title 屬性爲 "Bar"。這時候神奇的事情發生了,autorun 裏面傳入的函數又自動執行了一遍,控制檯輸出了新的 title 值 "Foo",到底發生了什麼?
        咱們先看下官方給咱們的解釋react

MobX 會對在 追蹤函數執行 過程讀取現存的可觀察屬性作出反應。

        嗯,看不懂。es6

        接下來官方又對上面這句話作了解釋:編程

  • 「讀取」是對象屬性的間接引用,能夠用過.(例如user.name) 或者[](例如user['name']) 的形式完成。
  • 「追蹤函數」computed表達式、observer 組件的render()方法和whenreactionautorun的第一個入參函數。
  • 「過程(during)」意味着只追蹤那些在函數執行時被讀取的 observable 。這些值是否由追蹤函數直接或間接使用並不重要。

        嗯,好像有點懂了,讓咱們從新分析下上面的代碼:app

// 這是可觀察對象
let message = observable({
    title: "Foo"
})

// autorun 是「追蹤函數」
autorun(() => {
    // message.title 是「讀取」操做
    // 此次讀取操做在函數執行「過程」中
    console.log(message.title)
})
// 輸出:
// "Foo"

message.title = "Bar"
// 輸出:
// "Bar"

        咱們聲明的 message 是一個可觀察對象,咱們註冊了一個 autorun 做爲追蹤函數,在這個追蹤函數中咱們傳入一個函數參數,這個函數進行了一次 message.title 的讀取操做,且此次操做在函數執行過程中。知足全部條件,bingo!!!異步

        可是你真的懂了嗎?async

        我再舉幾個例子,你們能夠根據上面的規則本身再判斷一下:
例1.ide

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message.title)
})
// 輸出:
// "Foo"

message = { title: "Bar" }

        上面我把 message.title = "Bar" 的賦值操做改成了直接修改 message 對象:message = { title: "Bar" },這時候 autorun 會執行嗎?

例2.

let message = observable({
    title: "Foo"
})

let title = message.title
autorun(() => {
    console.log(title)
})
// 輸出:
// "Foo"

message.title = "Bar"

        例2咱們新定義了一個 title = message.title 的變量,而後在 autorun 中輸出這個變量。

例3.

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message)
})

message.title = "Bar"

        例3咱們在 autorun 中直接輸出了 message 對象。

        上面3個例子都是不能在 message 的 title 變動的時候正常響應的:

  1. 例1由於 autorun 追蹤的是 message 對 title 屬性的讀取操做,可是咱們變動的是 message 引用,原 message 對象的 title 屬性並無發生變動,因此 autorun 不會自動執行;
  2. 例2由於 autorun 裏面並無 「讀取」 操做,因此不會追蹤 message.title 的變動;
  3. 例3可能難理解一些,由於 console.log(message) 實際上是 「讀取」 了 message 對象的全部屬性並輸出到控制檯上的,因此這裏知足了 「追蹤函數」「讀取」 兩個條件,既然還有問題,那確定是沒有知足 「過程」 這個條件。緣由就是 console.log 函數是異步的,它並無在函數的執行過程當中當即調用。

        是否是發現事情開始並得複雜了起來?

        如今讓咱們放慢一下腳步,中止對 Mobx 官方解釋的過分理解,這些只是 Mobx 實現者的文字遊戲,他們並無告訴咱們事情的本質。

依賴收集的實現

        讓咱們換一個角度,思考一下 Mobx 的依賴收集究竟是如何實現的?
        仍是上文的例子,這一次讓咱們剖析一下這段代碼的實現原理:

1  let message = observable({
2     title: "Foo"
3  })
4
5  autorun(() => {
6     console.log(message.title)
7  })
8  // 輸出:
9  // "Foo"
10
11 message.title = "Bar"
12 // 輸出:
13 // "Bar"

        上面的 1 到 3 行代碼咱們聲明瞭一個 message 對象,而且用 Mobx 的 observable 進行了封裝。這裏 observable 的意思就是讓 message 對象變成可觀察對象,observable 作的事情就是用 ES6 Proxy 代理了 { title: "Foo" } 這個普通對象並返回代理對象給 message。這樣 Mobx 就有能力去監聽 message 的變動了,咱們能夠本身實現一個 observable:

function observable(origin) {
  return new Proxy(origin, {
    // 監聽取值操做
    get: function (target, propKey, receiver) {
        // ...
        return Reflect.get(target, propKey, receiver);
    },
    // 監聽賦值操做
    set: function (target, propKey, value, receiver) {
        // ...
        return Reflect.set(target, propKey, value, receiver);
    }
  })
}

        第 5 到 7 行咱們傳入了一個函數參數調用了 autorun,函數參數只是簡單輸出 message 的 title 屬性到控制檯。通過這一步之後咱們在 11 行修改了 message 的 title 屬性,autorun 的註冊函數就會自動執行,在控制檯輸出最新的 message.title 信息。
        再從新看一下上面的代碼,思考一個問題:autorun 爲何會知道它須要去關心 message 對象的 title 屬性?咱們沒有傳相似 ["message", "title"] 這樣明確的參數給他,它接受的惟一參數只是一個執行函數,看起來就好像它自動去解析了執行函數的函數體內容,這就像個魔術同樣。
        Mobx 的執行確實像魔術同樣神奇,可是就像不少魔術的原理都很簡單,Mobx 的依賴收集原理也很簡單。解開這個魔術的鑰匙就是 「全局變量」
        聯繫一下上面提供的幾個線索:

  1. message 對象是一個 Proxy 對象;
  2. autorun 註冊了一個執行函數,執行函數內部有 message.title 的 get 操做;
  3. 對 message.title 進行 set,autorun 的註冊函數自動運行;

        讓咱們解開 autorun 的祕密:

function autorun(trigger) {
  window.globalState = trigger
  trigger()
  window.globalState = null
}

        autorun 函數先將接收的執行函數掛載到 globalState 的全局變量上,接下來當即觸發一次執行函數,最後將 globalState 重置爲 null。
        咱們再改寫一下咱們的 observable 函數:

function observable(origin) {
  let listeners = {}
  return new Proxy(origin, {
    // 監聽取值操做
    get: function (target, propKey, receiver) {
        if(window.globalState) {
          listeners[propKey] = listeners[propKey] || []
          listeners[propKey] = [...listeners, window.globalState]
        }
        return Reflect.get(target, propKey, receiver);
    },
    // 監聽賦值操做
    set: function (target, propKey, value, receiver) {
        listeners[propKey].forEach((fn) => fn())
        return Reflect.set(target, propKey, value, receiver);
    }
  })
}

        新的 observable 函數維護了一個事件隊列,在每次對象屬性的取值操做時去檢查全局的 globalState 屬性,若是發現當前取值操做是在一個追蹤函數內執行的,就將 globalState 的值放入事件隊列中;在每次對象的賦值操做發生時執行一遍事件隊列。
        上面的 observable 和 autorun 只用於解釋基本原理,不表明 Mobx 的真實實現。

        如今咱們對 Mobx 的依賴收集有了更深入的理解,再讓咱們回過頭去看一下比較難理解的例3:

let message = observable({
    title: "Foo"
})

autorun(() => {
    console.log(message)
})

message.title = "Bar"

        這裏的關鍵在於 console.log 是一個異步的函數,將它代入 autorun:

function autorun(trigger) {
  window.globalState = trigger
  trigger()
  window.globalState = null
}

autorun(() => {
  console.log(message)
})

        讓咱們解構一下函數執行:

window.globalState = () => console.log(message)
// async
console.log(message)
window.globalState = null

        假設有一個 print 函數能夠在控制檯同步輸出信息,由於 console.log 是異步的,上面的代碼執行會變成:

window.globalState = () => console.log(message)
window.globalState = null
print(`{ message: ${ message.title } }`)

        雖然 message.title 作了一次 get 操做,但這時候的 globalState 已經變成 null 了,message 對象的事件隊列固然不能註冊到這個執行函數。下次遇到相似的問題,你均可以試着把執行函數代入到 autorun 中分析一下,結果就能一目瞭然了。

        Mobx 對於 autorun 的說明也從側面驗證了咱們上面的實現:

當使用 autorun時,所提供的函數老是當即被觸發一次,而後每次它的依賴關係改變時會再次被觸發。

What does mobx react to?

        那麼 Mobx 對於什麼會作出響應,你如今比之前更清楚一些了嗎?

相關文章
相關標籤/搜索