Mobx 最關鍵的函式在於 autoRun
,舉個例子,它可以達到這樣的效果:
1 2 3 4 5 6 7 8 9 10 11 |
const obj = observable({ a: 1, b: 2 }) autoRun(() => { console.log(obj.a) }) obj.b = 3 // 什麼都沒有發生 obj.a = 2 // observe 函式的回撥觸發了,控制檯輸出:2 |
我們發現這個函式非常智慧,用到了什麼屬性,就會和這個屬性掛上鉤,從此一旦這個屬性發生了改變,就會觸發回撥,通知你可以拿到新值了。沒有用到的屬性,無論你怎麼修改,它都不會觸發回撥,這就是神奇的地方。
autoRun 的用途
使用 autoRun
實現 mobx-react
非常簡單,核心思想是將元件外面包上 autoRun
,這樣程式碼中用到的所有屬性都會像上面 Demo 一樣,與當前元件繫結,一旦任何值發生了修改,就直接 forceUpdate
,而且精確命中,效率最高。
依賴收集
autoRun
的專業名詞叫做依賴收集,也就是通過自然的使用,來收集依賴,當變數改變時,根據收集的依賴來判斷是否需要更新。
實現步驟拆解
為了相容,Mobx 使用了 Object.defineProperty
攔截 getter
和 setter
,但是無法攔截未定義的變數,為了方便,我們使用 proxy
來講解,而且可以監聽未定義的變數哦。
步驟一 儲存結構
眾所周知,事件監聽是需要預先儲存的,autoRun
也一樣,為了知道當變數修改後,哪些方法應該被觸發,我們需要一個儲存結構。
首先,我們需要儲存所有的代理物件,讓我們無論拿到原始物件,還是代理物件,都能快速的找出是否有對應的代理物件存在,這個功能用在判斷代理是否存在,是否合法,以及同一個物件不會生成兩個代理。
程式碼如下:
1 2 3 4 5 |
const proxies = new WeakMap() function isObservable<T extends object>(obj: T) { return (proxies.get(obj) === obj) } |
重點來了,第二個要儲存的是最重要的部分,也就是所有監聽!當任何物件被改變的時候,我們需要知道它每一個 key
對應著哪些監聽(這些監聽由 autoRun
註冊),也就是,最終會存在多個物件,每個物件的每個 key
都可能與多個 autoRun
繫結,這樣在更新某個 key
時,直接觸發與其繫結的所有 autoRun
即可。
程式碼如下:
1 |
const observers = new WeakMap<object, Map<PropertyKey, Set<Observer>>>() |
第三個儲存結構就是待觀察佇列,為了使同一個呼叫棧多次賦值僅執行一次 autoRun
,所有待執行的都會放在這個佇列中,在下一時刻統一執行佇列並清空,執行的時候,當前所有 autoRun
都是在同一時刻觸發的,所以讓相同的 autoRun
不用觸發多次即可實現效能優化。
1 |
const queuedObservers = new Set() |
程式碼如下:
我們還要再儲存兩個全域性變數,分別是是否在佇列執行中,以及當前執行到的 autoRun
。
程式碼如下:
1 2 |
let queued = false let currentObserver: Observer = null |
步驟二 將物件加工可觀察
這一步講解的是 observable
做了哪些事,首先第一件就是,如果已經存在代理物件了,就直接返回。
程式碼如下:
1 2 3 |
function observable<T extends object>(obj: T = {} as T): T { return proxies.get(obj) || toObservable(obj) } |
我們繼續看 toObservable
函式,它做的事情是,例項化代理,並攔截 get
set
等方法。
我們先看攔截 get
的作用:先拿到當前要獲取的值 result
,如果這個值在代理中存在,優先返回代理物件,否則返回 result
本身(沒有引用關係的基本型別)。
上面的邏輯只是簡單返回取值,並沒有註冊這一步,我們在 currentObserver
存在時才會給物件當前 key
註冊 autoRun
,並且如果結果是物件,又不存在已有的代理,就呼叫自身 toObservable
再遞迴一遍,所以返回的物件一定是代理。
registerObserver
函式的作用是將 targetObj -> key -> autoRun
這個鏈路關係存到 observers
物件中,當物件修改的時候,可以直接找到對應 key
的 autoRun
。
那麼 currentObserver
是什麼時候賦值的呢?首先,並不是訪問到 get
就要註冊 registerObserver
,必須在 autoRun
裡面的才符合要求,所以執行 autoRun
的時候就會將當前回撥函式賦值給 currentObserver
,保證了在 autoRun
函式內部所有監聽物件的 get
攔截器都能訪問到 currentObserver
。以此類推,其他 autoRun
函式回撥函式內部變數 get
攔截器中,currentObserver
也是對應的回撥函式。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const dynamicObject = new Proxy(obj, { // ... get(target, key, receiver) { const result = Reflect.get(target, key, receiver) // 如果取的值是物件,優先取代理物件 const resultIsObject = typeof result === 'object' && result const existProxy = resultIsObject && proxies.get(result) // 將監聽新增到這個 key 上 if (currentObserver) { registerObserver(target, key) if (resultIsObject) { return existProxy || toObservable(result) } } return existProxy || result }), // ... }) |
setter
過程中,如果物件產生了變動,就會觸發 queueObservers
函式執行回撥函式,這些回撥都在 getter
中定義好了,只需要把當前物件,以及修改的 key
傳過去,直接觸發對應物件,當前 key
所註冊的 autoRun
即可。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const dynamicObject = new Proxy(obj, { // ... set(target, key, value, receiver) { // 如果改動了 length 屬性,或者新值與舊值不同,觸發可觀察佇列任務 if (key === 'length' || value !== Reflect.get(target, key, receiver)) { queueObservers<T>(target, key) } // 如果新值是物件,優先取原始物件 if (typeof value === 'object' && value) { value = value.$raw || value } return Reflect.set(target, key, value, receiver) }, // ... }) |
沒錯,主要邏輯已經全部說完了,新物件之所以可以檢測到,是因為 proxy
的 get
會觸發,這要多謝 proxy
的強大。
可能有人問 Object.defineProperty
為什麼不行,原因很簡單,因為這個函式只能設定某個 key
的 getter
setter
~。
symbol
proxy
reflect
這三劍客能做的事還有很多很多,這僅僅是實現 Object.observe
而已,還有更強大的功能可以挖掘。
總結
es6 真的非常強大,呼籲大家拋棄 ie11,擁抱美好的未來!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式