Mobx 原始碼閱讀簡記
Mobx 原始碼簡記
整體會寫得比較亂,同時也比較簡單,和讀書筆記差不多,基本是邊讀邊寫。見諒~
主要三大部分Atom
、Observable
、Derivation
Atom
Mobx的原子類,能夠被觀察和通知變化,observableValue繼承於Atom。observableValue ---> Atom
同時裡面有幾個比較重要的屬性與方法。
-
屬性
- observers,用於存放這個被原子類被誰觀察了,是一個set結構
- diffValue,後續更新依賴的時候要用這個來判斷
-
方法
- reportObserved,呼叫全域性的reportObserved函式,通知自身被觀察了
- reportChanged,呼叫全域性的propagateChanged函式,通知自身發生變化了
Observable
Observable是一個工廠函式,讓資料變得可觀察。這個東西需要和上述的Atom 建立聯絡,即將具體的值 與Atom 聯絡起來。從而打通自身能夠被觀察,同時能通知變化的整個流程。
三種可被觀察的資料型別:物件,陣列,Map,下面簡單介紹如何實現。假如都不是,就會提示使用者呼叫observable.box,使其擁有get,set方法,即上述說的observableValue 資料型別。
部分程式碼如下:
fucntion Observable(v) { // 如果已被觀察,直接返回 if (isObservable(v)) return v // 根據其型別分別呼叫observable.object、observable.array、observable.map const 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 // 都不是,提示使用者呼叫observable.box(value) 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)'` ) }
重點是observable.object、observable.array、observable.map三者的實現,下面是討論關於物件的實現方式
-
物件(observable.object)
- 先建立一個base物件,我們稱為adm物件。同時,根據這個base物件建立一個proxy,會通過該proxy將會對原物件的各種值進行代理,而adm[$mobx]指向該一個新建的ObservableObjectAdministration 資料型別
- 對傳進來的props(即需要被觀察的物件),會先尋找是否有get屬性(即計算屬性),有的話會建立一個計算屬性代理,並和其餘的屬性一起掛載在該proxy上
-
有計算屬性時,會新建一個既有observableValue 也有derivation 屬性的computedValue類,存放到adm[$mobx].values裡面,key就是computed的key
- 同時會拿到它的get函式,作為這個derivation 的監聽函式,進行初始化監聽
- 並通過Object.defineProperty設定了該屬性的get和set屬性
- 其餘的屬性,會新建一個observableValue ,存放到adm[$mobx].values裡面,並通過Object.defineProperty設定了該屬性的get和set屬性
-
然後,重點是建立proxy時的handler物件的get 和set 函式,在有新屬性訪問時或改變值時會呼叫get 和set 函式
- 訪問新屬性時,get函式會讀取adm[$mobx],如果沒有,會通過has方法,建立一個**observableValue**,並放到adm[\$mobx].pendingKeys中(還不知道有什麼用)
- 設定新屬性時,會新建一個observableValue 存放進去adm[$mobx].values中,同時,通過Object.defineProperty設定了該屬性的get和set屬性
重點:
(observableValue簡稱為oV,Object.defineProperty簡稱為Od)
- 上面說的所有通過Od定義後的set會呼叫已存放的oV 的set ,get會呼叫已存放的oV 的get
- 第一點說過oV 繼承於Atom ,所以oV 的set 會呼叫reportChanged,oV 的get 會呼叫reportObserved
這樣子,整個物件屬性的監聽流程就建立起來了
Reaction
Reaction(反應)是一類特殊的Derivation,可以註冊響應函式,使之在條件滿足時自動執行。使用如下:
// new Reaction(name, onInvalidate) const reaction = new Reaction('name', () => { // do something,即響應函式,發生副作用的地方 console.log('excuted!') }) const ob = observable.object({ name: 'laji', key: '9527' }) reaction.track(() => { // 註冊需要被追蹤的變數,這裡訪問了已經被觀察的ob物件,所以當ob.name或ob.key發生改變時,上面的響應函式會執行 console.log(`my name is ${ob.name}`) console.log(`${ob.key} hey hey hey!`) }) ob.name = 'mike' // 'excuted!'
讓我們分析一下原始碼實現,主要有幾個重點:
- 初始化Reaction類時,會將onInvalidate函式儲存起來
-
在呼叫track 函式時,這個是重點,會呼叫trackDerivedFunction(this, fn, undefined)
- trackDerivedFunction 這個函式,就是依賴收集,即註冊需要被追蹤的變數,它會做幾件事情,看下面註釋
export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) { // 將該 Derivation 的 dependenciesState 和當前所有依賴的 lowestObserverState 設為最新的狀態 changeDependenciesStateTo0(derivation) // 建立一個該derivation新的newObserving陣列,裡面存放的是誰被該derivation註冊依賴了 derivation.newObserving = new Array(derivation.observing.length + 100) // 記錄新的依賴的數量 derivation.unboundDepsCount = 0 // 每次執行都分配一個全域性的 uid derivation.runId = ++globalState.runId // 重點,將當前的derivation分配為全域性的globalState.trackingDerivation,這樣被觀察的 Observable 在其 reportObserved 方法中就能獲取到該 Derivation const prevTracking = globalState.trackingDerivation globalState.trackingDerivation = derivation let result // 下面執行存入track的函式,觸發被觀察變數的get方法 if (globalState.disableErrorBoundaries === true) { result = f.call(context) } else { try { result = f.call(context) } catch (e) { result = new CaughtException(e) } } globalState.trackingDerivation = prevTracking // 比較新舊依賴,更新依賴 bindDependencies(derivation) return result }
可以看到,重點有兩個,一個是將當前的derivation分配為全域性的globalState.trackingDerivation ,一個是下面的更新依賴過程。
-
接下來,我們看看觸發了被觀察變數的get方法,會是怎樣的,上面說過,呼叫get方法會執行reportObserved函式
export function reportObserved(observable: IObservable): boolean { // 拿到剛才被設定到全域性的derivation const derivation = globalState.trackingDerivation if (derivation !== null) { if (derivation.runId !== observable.lastAccessedBy) { observable.lastAccessedBy = derivation.runId // 這行是重點,將被觀察的變數,放到derivation.newObserving陣列中,因此,derivation裡就存放了這次訪問中被觀察的變數 derivation.newObserving![derivation.unboundDepsCount++] = observable if (!observable.isBeingObserved) { observable.isBeingObserved = true observable.onBecomeObserved() } } return true } else if (observable.observers.size === 0 && globalState.inBatch > 0) { queueForUnobservation(observable) } return false }
-
之後是bindDependencies 函式的執行。這裡面有兩點,不做程式碼解讀了:
- 一是主要是比較derivation的新舊observing(存放被觀察變數的陣列),防止重複記錄,同時去除已過期的被觀察變數
- 二是,observable(被觀察的變數)的observers(是一個Set結構)更新裡面存放的derivation,即記錄自身被誰觀察了,在後面呼叫reportChanged時,觸發響應函式
被觀察的變數發生變化時
此時會呼叫observable的set函式,然後呼叫reportChanged,最終會呼叫一個叫做propagateChanged 函式。
export function propagateChanged(observable: IObservable) { // 已經在運行了,直接返回 if (observable.lowestObserverState === IDerivationState.STALE) return observable.lowestObserverState = IDerivationState.STALE // 上面說過,observable(被觀察的變數)的observers存放著derivation // 這裡就是執行每個derivation的onBecomeStale函式 observable.observers.forEach(d => { if (d.dependenciesState === IDerivationState.UP_TO_DATE) { if (d.isTracing !== TraceMode.NONE) { logTraceInfo(d, observable) } d.onBecomeStale() } d.dependenciesState = IDerivationState.STALE }) }
onBecomeStale最終會呼叫derivation裡的schedule 函式,裡面做了兩件事:
- 把自身推進全域性的globalState.pendingReactions 陣列
-
執行runReactions函式
- 該函式就核心就做一件事情,遍歷globalState.pendingReactions陣列,執行裡面每個derivation的runReaction函式
- runReaction最終會呼叫derivation自身的onInvalidate,即響應函式
至此,整個mobx的資料觀察與響應流程就都一一解釋完整了(autorun,autorunAsync,when等函式都是基於Reaction來實現的,就不作過多解讀了)
Mobx-React原始碼簡記
既然mobx都說了,那就把mobx-react也分析一下吧。其實很簡單,只要理解了Reaction與Observable,就很容易明白mobx-react的實現了。
mobx-react的實現主要也是兩點
- 通過provide和inject,將已經被觀察過的observerableStore集中起來並按需分配到所需要的元件中
- 被observer的元件,改寫其render函式,使其可以響應變化
第一點比較簡單,實現一個hoc,把observerableStore新增到context上,然後被inject的元件就可以拿到所需的observerableStore
我們重點看下第二點,實現第二點的主要邏輯,在observer.js
裡面的makeComponentReactive
函式中,看下面簡化版的重點解析
// makeComponentReactive function makeComponentReactive(render) { if (isUsingStaticRendering === true) return render.call(this) // 改造後的render函式 function reactiveRender() { // 防止重複執行響應函式,因為componentWillReact有可能有副作用 isRenderingPending = false // render函式執行後返回的jsx let rendering = undefined // 註冊需要被追蹤的變數 reaction.track(() => { if (isDevtoolsEnabled) { this.__$mobRenderStart = Date.now() } try { // _allowStateChanges是安全地執行原來的render函式,假如在action外有更改變數的行為,會報錯 // 重點是這個,因為render函式被執行了,所以假如裡面有被observe過的變數,就能被追蹤,更新到依賴該reaction的依賴列表裡面 rendering = _allowStateChanges(false, baseRender) } catch (e) { exception = e } if (isDevtoolsEnabled) { this.__$mobRenderEnd = Date.now() } }) return rendering } // ....省略一些程式碼 // 新建一個Reaction,註冊響應函式 const reaction = new Reaction(`${initialName}#${rootNodeID}.render()`, () => { if (!isRenderingPending) { // 正在執行響應函式 isRenderingPending = true // 這裡就是執行新的componentWillReact生命週期的地方 if (typeof this.componentWillReact === "function") this.componentWillReact() if (this.__$mobxIsUnmounted !== true) { let hasError = true try { setHiddenProp(this, isForcingUpdateKey, true) // 也是重點,通過forceUpdate,更新元件 if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this) hasError = false } finally { setHiddenProp(this, isForcingUpdateKey, false) if (hasError) reaction.dispose() } } } }) // 改寫原來的render reaction.reactComponent = this reactiveRender[mobxAdminProperty] = reaction this.render = reactiveRender return reactiveRender.call(this) }
可以見到,通過建立一個Reaction,實現了render函式裡的被觀察的變數收集及響應函式註冊。而且在通過forceUpdate重新更新元件後,render函式會被重新執行一遍,從而實時更新被觀察的變數 。整體的實現還是巧妙的。
除此之外,還有一些生命週期的優化,對props、state也進行監聽等操作,在這裡就不一一解讀了