Mobx 是一個通過戰火洗禮的庫,它經過透明的 函數響應式編程(transparently applying functional reactive programming - TFRP)使得 狀態管理變得簡單和可擴展.
上面這段話引自 Mobx 的官方文檔,說明了 Mobx 是一個應用了函數響應式的狀態管理庫。所謂的響應式就是事件監聽,也是 Mobx 背後的哲學:javascript
任何源自應用狀態的東西都應該自動地得到
這裏說的 「應用狀態」 就是 state,在 Mobx 的世界裏叫 observable;源自應用狀態的 「東西」 叫作 derivations,derivations 能夠分爲兩大類:computed 和 reaction 。
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()
方法和when
、reaction
和autorun
的第一個入參函數。- 「過程(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 變動的時候正常響應的:
是否是發現事情開始並得複雜了起來?
如今讓咱們放慢一下腳步,中止對 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 的依賴收集原理也很簡單。解開這個魔術的鑰匙就是 「全局變量」。
聯繫一下上面提供的幾個線索:
讓咱們解開 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
時,所提供的函數老是當即被觸發一次,而後每次它的依賴關係改變時會再次被觸發。
那麼 Mobx 對於什麼會作出響應,你如今比之前更清楚一些了嗎?