Mobx 源碼解讀(二) Observable
Mobx 提供了三種可觀察的數據類型:對象、數組和 Map。Mobx 內部做了大量的工作,使它們的使用體驗和原生 JS 類型一致,通過 observable api 包裝後就可以轉換成可觀察值,使用時無須額外的方法調用。對於其它類型的值,Mobx 提供了 observable.box,包裝之後使用其 get, set 方法來獲取和設置值,也可以達到「可觀察」的效果。
第一篇中提到,Observable 使用 reportObserved 和 propagateChanged 函數通知自身「被觀察」和「發生變化」。將值變得可觀察的關鍵步驟就在於觸發這兩個函數的調用,先來看看不同類型的 Observable 是如何實現這一點的。
如何變得「可觀察」
Mobx 提供的 API observable,實際上是一個工廠函數 createObservable:
function createObservable(v: any = undefined) { // observable 作為屬性裝飾器使用 // 默認使用 deepDecorator,它是 deepEnhancer 對應的 Decorator if (typeof arguments[1] === "string") return deepDecorator.apply(null, arguments) if (isObservable(v)) return v // 默認使用 deepEnhancer 包裝 const res = deepEnhancer(v, undefined, undefined) // 包裝後轉換為了「可觀察的」的數據結構 if (res !== v) return res // 否則,調用 observable.box 包裝 return observable.box(v)}// 導出 observable apiexport const observable: IObservableFactory & IObservableFactories & { deep: { struct<T>(initialValue?: T): T } ref: { struct<T>(initialValue?: T): T } } = createObservable as any
deepEnhancer 是 Mobx 默認的 Modifier(關於 Modifier 可以參看第三篇),會對對象、數組和 Map 進行處理:
function deepEnhancer(v, _, name) { if (isObservable(v)) return v if (Array.isArray(v)) return observable.array(v, name) if (isPlainObject(v)) return observable.object(v, name) if (isES6Map(v)) return observable.map(v, name) return v}
可以看到,對於其他類型的值,deepEnhancer 原樣返回,從而在 createObservable 中會使用 observable.box 進行包裝。先來看看最簡單的 observable.box 如何使傳入的值可觀察。
Box
observable.box 方法簡單地返回一個 ObservableValue 實例。
box<T>(value?: T, name?: string): IObservableValue<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("box") return new ObservableValue(value, deepEnhancer, name)}
ObservableValue 實現了 get 和 set 方法,用戶在使用時自行使用這兩個方法獲取和設置「可觀察原始值」的值,那麼實現「可觀察」就只需在這兩個方法內分別去調用 reportObserved 和 reportChanged 即可:
public set(newValue: T) { const oldValue = this.value newValue = this.prepareNewValue(newValue) as any if (newValue !== UNCHANGED) { // ... // 發生變化,設置新值 this.setNewValue(newValue) }}// 做一些新值的預處理工作private prepareNewValue(newValue): T | IUNCHANGED { // 檢查兩個邊界情況 checkIfStateModificationsAreAllowed(this) // ... // 經過 Enhancer 包裝 newValue = this.enhancer(newValue, this.value, this.name) return this.value !== newValue ? newValue : UNCHANGED}setNewValue(newValue: T) { const oldValue = this.value this.value = newValue // reportChanged 方法繼承自 BaseAtom this.reportChanged()}public get(): T { // reportObserved 方法繼承自 BaseAtom this.reportObserved() // 使用 Enhancer 相應的 Dehancer 獲取包裝前的值 return this.dehanceValue(this.value)}
對象
observable.object 方法將對象變為可觀察的,它實際上是把對象的所有屬性轉換為可觀察的,存放到一個代理對象上,以減少對原對象的污染:
// observable.objectobject<T>(props: T, name?: string): T & IObservableObject { if (arguments.length > 2) incorrectlyUsedAsDecorator("object") const res = {} // 創建代理對象 asObservableObject(res, name) // 添加可觀察屬性 extendObservable(res, props) return res as any}
asObservableObject 函數為對象創建一個「可觀察對象」作為代理對象,按照源碼中的命名,我們稱之為「admin 對象」。admin 對象的 target 屬性指向原對象,並作為隱藏屬性添加到原對象上。它的實現如下:
function asObservableObject(target, name?: string): ObservableObjectAdministration { // admin 對象已經存在,直接返回 if (isObservableObject(target) && target.hasOwnProperty("$mobx")) return (target as any).$mobx if (!isPlainObject(target)) name = (target.constructor.name || "ObservableObject") + "@" + getNextId() if (!name) name = "ObservableObject@" + getNextId() // 創建 admin 對象 const adm = new ObservableObjectAdministration(target, name) // 作為隱藏屬性添加到原對象上 addHiddenFinalProp(target, "$mobx", adm) return adm}
admin 對象通過 addHiddenFinalProp 方法,作為一個隱藏屬性添加到原對象上。使用 ES5 的 Object.defineProperty 定義一個不可枚舉的屬性即可實現:
function addHiddenProp(object: any, propName: string, value: any) { Object.defineProperty(object, propName, { enumerable: false, writable: true, configurable: true, value })}
admin 對象的 values 屬性上用於存放原對象的屬性,不同的是,這些屬性都是經過 Enhancer 包裝過的可觀察屬性。
接下來就是調用 extendObservable 來生成這些可觀察屬性:
function extendObservable<A extends Object, B extends Object>( target: A, ...properties: B[] // 可以接受多組 props): A & B { // 使用 deepEnhancer,對複雜類型的屬性會遞歸執行 return extendObservableHelper(target, deepEnhancer, properties) as any}function extendObservableHelper( target: Object, defaultEnhancer: IEnhancer<any>, properties: Object[]): Object { const adm = asObservableObject(target) // 避免多組 props 可能有重複 const definedProps = {} for (let i = properties.length - 1; i >= 0; i--) { const propSet = properties[i] for (let key in propSet) if (definedProps[key] !== true && hasOwnProperty(propSet, key)) { definedProps[key] = true if ((target as any) === propSet && !isPropertyConfigurable(target, key)) continue // #111 const descriptor = Object.getOwnPropertyDescriptor(propSet, key) // 根據屬性描述符定義可觀察屬性 defineObservablePropertyFromDescriptor(adm, key, descriptor, defaultEnhancer) } } return target}
defineObservablePropertyFromDescriptor 函數根據屬性描述符來定義可觀察屬性,它是一個通用的函數,後面我們還會多次看到這個函數。對於對象屬性,會進入 defineObservableProperty 分支:
function defineObservablePropertyFromDescriptor( adm: ObservableObjectAdministration, propName: string, descriptor: PropertyDescriptor, defaultEnhancer: IEnhancer<any>) { // 已經是可觀察屬性 if (adm.values[propName] && !isComputedValue(adm.values[propName])) { invariant( "value" in descriptor, `The property ${propName} in ${adm.name} is already observable, cannot redefine it as computed property` ) adm.target[propName] = descriptor.value return } // 該屬性是數據屬性 if ("value" in descriptor) { // 如果是 Modifier 的描述符,取出值和enhancer if (isModifierDescriptor(descriptor.value)) { const modifierDescriptor = descriptor.value as IModifierDescriptor<any> defineObservableProperty( adm, propName, modifierDescriptor.initialValue, modifierDescriptor.enhancer ) // Action } else if (isAction(descriptor.value) && descriptor.value.autoBind === true) { defineBoundAction(adm.target, propName, descriptor.value.originalFn) // 計算值 } else if (isComputedValue(descriptor.value)) { defineComputedPropertyFromComputedValue(adm, propName, descriptor.value) } else { // 定義可觀察屬性 defineObservableProperty(adm, propName, descriptor.value, defaultEnhancer) } // 訪問器屬性(即計算值) } else { defineComputedProperty( adm, propName, descriptor.get, descriptor.set, comparer.default, true ) }}
defineObservableProperty 為對象定義可觀察屬性:
function defineObservableProperty( adm: ObservableObjectAdministration, propName: string, newValue, enhancer: IEnhancer<any>) { // ... // 將屬性定義成一個 ObservableValue 實例,存放在 admin 對象的 values 屬性上 // 注意這裡使用的是 deepEnhancer,意味著對於屬性值是複雜對象的情形,在實例化 // ObservableValue 時會遞歸執行「將對象轉換為可觀察對象」的過程,從而使得 values // 上的屬性都是可觀察的 const observable = (adm.values[propName] = new ObservableValue( newValue, enhancer, `${adm.name}.${propName}`, false )) newValue = (observable as any).value // 定義訪問器屬性 Object.defineProperty(adm.target, propName, generateObservablePropConfig(propName))}
重點來了,前面提到,ObservableValue 實現了 get 和 set 方法,對於原始值,由用戶負責主動調用這兩個方法,從而觸發 reportObserved 和 reportChanged(實際上是在 setNewValue 方法中觸發)。
那麼對於可觀察屬性,將其包裝成 ObservableValue 實例存放在 admin 對象上之後,如何觸發 get 和 setNewValue 方法呢?答案是利用訪問器屬性,generateObservablePropConfig 為屬性生成訪問器屬性描述符,在 get 和 set 訪問器中觸發相應 ObservableValue 實例的 get 和 setNewValue 方法:
const observablePropertyConfigs = {} // 屬性描述符緩存function generateObservablePropConfig(propName) { return ( observablePropertyConfigs[propName] || (observablePropertyConfigs[propName] = { configurable: true, enumerable: true, get: function() { // 調用 admin 對象上存放的相應 ObservableValue 實例的 get 方法 // 從而觸發 reportObserved return this.$mobx.values[propName].get() }, set: function(v) { setPropertyValue(this, propName, v) } }) )}function setPropertyValue(instance, name: string, newValue) { const adm = instance.$mobx const observable = adm.values[name] // 預處理 newValue = observable.prepareNewValue(newValue) // 值發生變化 if (newValue !== UNCHANGED) { // setNewValue 會觸發 reportChanged observable.setNewValue(newValue) }}
經歷完這一系列步驟後,最後返回的就是一個可觀察的對象了。
還有一點需要注意的是,因為 extendObservable 中使用的是 deepEnhancer,意味著在 defineObservableProperty 函數中,實例化 ObservableValue 時,如果該屬性的值是複雜對象,會遞歸執行「將對象轉換為可觀察的」過程,從而使得該屬性的所有屬性(或元素)也都是可觀察的:
// ObservableValue 的構造函數constructor( value: T, protected enhancer: IEnhancer<T>, name = "ObservableValue@" + getNextId(), notifySpy = true) { super(name) // 遞歸調用 deepEnhancer this.value = enhancer(value, undefined, name) // ...}
數組
Mobx 實現了一個擴展的數組類型,ObservableArray,來支持數組的可觀察。observable.array 方法返回一個 ObservableArray 實例:
// observable.array 工廠方法array<T>(initialValues?: T[], name?: string): IObservableArray<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("array") // 返回一個 ObservableArray 實例,使用 deepEnhancer 對各數組項進行包裝 return new ObservableArray(initialValues, deepEnhancer, name) as any}
與對象不同的是,數組的「屬性名」都是一樣的,即0,1,2...這樣的索引。Mobx 利用原型鏈,在 ObservableArray 的原型上添加 0,1,2...等訪問器屬性:
// 記錄 Observable.prototype 上訪問器屬性的數量let OBSERVABLE_ARRAY_BUFFER_SIZE = 0// 新增訪問器屬性function reserveArrayBuffer(max: number) { for (let index = OBSERVABLE_ARRAY_BUFFER_SIZE; index < max; index++) createArrayBufferItem(index) OBSERVABLE_ARRAY_BUFFER_SIZE = max}function createArrayBufferItem(index: number) { Object.defineProperty(ObservableArray.prototype, "" + index, createArrayEntryDescriptor(index))}// 訪問器屬性描述符function createArrayEntryDescriptor(index: number) { return { enumerable: false, configurable: false, get: function() { // 調用 ObservableArray 實例的 get 方法,帶上存放在閉包內的索引 return this.get(index) }, set: function(value) { // 調用 ObservableArray 實例的 set 方法 this.set(index, value) } }}// 初始化時先添加1000個索引屬性reserveArrayBuffer(1000)
這樣,使用索引訪問 ObservableArray 實例中的元素時,順著原型鏈查找到相應屬性,就會調用 ObservableArray 實例的 get 和 set 方法了,同時帶上相應的索引值。
Mobx 初始化時,會在 ObservableArray.prototype 上添加1000個這樣的索引屬性,當數組長度超過1000時,再通過 reserveArrayBuffer 函數來擴充 ObservableArray.prototype 上索引屬性的數量。
也就是說,ObservableArray 實例上並沒有0,1,2...等屬性,那麼數組項存放在哪呢?
和對象的處理類似,每一個 ObservableArray 實例都有一個對應的 ObservableArrayAdministration 實例來管理數組項,數組的每一項都會轉換成「可觀察的」之後,存放在 admin 對象的 values 屬性上,這個屬性是一個原生 JS 數組。
ObservableArray, ObservableArray.prototype, ObservableArrayAdministration 三者之間的關係如下圖所示:
來看看一個可觀察的數組的初始化過程:
// ObservableArray 的構造函數constructor( initialValues: T[] | undefined, enhancer: IEnhancer<T>, name = "ObservableArray@" + getNextId(), owned = false) { super() // 創建 admin 對象,並作為不可枚舉、不可修改的屬性添加到 ObservableArray 實例上 const adm = new ObservableArrayAdministration<T>(name, enhancer, this as any, owned) addHiddenFinalProp(this, "$mobx", adm) // 添加傳入的數組項到 admin 對象的 values 屬性上 if (initialValues && initialValues.length) { this.spliceWithArray(0, 0, initialValues) }}
spliceWithArray 方法直接調用了 admin 對象上的同名方法:
// ObservableArrayAdministration.prototype.spliceWithArrayspliceWithArray(index: number, deleteCount?: number, newItems?: T[]): T[] { checkIfStateModificationsAreAllowed(this.atom) const length = this.values.length // 一堆參數的處理 if (index === undefined) index = 0 else if (index > length) index = length else if (index < 0) index = Math.max(0, length + index) if (arguments.length === 1) deleteCount = length - index else if (deleteCount === undefined || deleteCount === null) deleteCount = 0 else deleteCount = Math.max(0, Math.min(deleteCount, length - index)) if (newItems === undefined) newItems = [] // 重點:遍歷新增的數組項,返回經過 enhancer 包裝後的 newItems = <T[]>newItems.map(v => this.enhancer(v, undefined)) const lengthDelta = newItems.length - deleteCount // 更新 length this.updateArrayLength(length, lengthDelta) // 更新 values 數組 const res = this.spliceItemsIntoValues(index, deleteCount, newItems) if (deleteCount !== 0 || newItems.length !== 0) // 通知變化 this.notifyArraySplice(index, newItems, res) // 和原生 splice 方法一樣,返回刪除的數組項 return this.dehanceValues(res)}
這樣就得到了一個可觀察的數組。
當我們訪問數組項時,如前文所述,Observable.prototype 上的索引屬性被訪問,並通過 get 訪問器調用 ObservableArray 實例的 get 方法:
get(index: number): T | undefined { // 獲取 admin 對象 const impl = <ObservableArrayAdministration<any>>this.$mobx if (impl) { if (index < impl.values.length) { // admin 對象上的 Atom 實例通知數組被觀察 impl.atom.reportObserved() // 返回經過 dehancer 處理的原始數組項 return impl.dehanceValue(impl.values[index]) } } return undefined}
直接通過索引修改數組項的值時,set 訪問器會調用 ObservableArray 實例的 set 方法,同樣也是由 admin 對象負責數組項的更新和變化通知:
set(index: number, newValue: T) { const adm = <ObservableArrayAdministration<T>>this.$mobx const values = adm.values if (index < values.length) { checkIfStateModificationsAreAllowed(adm.atom) const oldValue = values[index] newValue = adm.enhancer(newValue, oldValue) const changed = newValue !== oldValue if (changed) { values[index] = newValue // notifyArrayChildUpdate 方法內會調用 atom 實例的 reportChanged 方法發送變化通知 adm.notifyArrayChildUpdate(index, newValue, oldValue) } } else if (index === values.length) { // 添加一個元素 adm.spliceWithArray(index, 0, [newValue]) } else { // 拋出錯誤... }}
Mobx 中數組的 length 屬性也是可觀察的,原理也是一樣的,都是利用原型鏈,在 Observable.prototype 上定義有 length 訪問器屬性,這裡不再贅述。
為了得到類似原生數組的使用體驗,ObservableArray 實現了所有原生數組的方法。來看看具體的實現方式。
對於 map, slice, toString 等不修改原數組的方法,只是在調用之前發送被觀察通知:
;[ "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", "map", "reduce", "reduceRight", "slice", "some", "toString", "toLocaleString"].forEach(funcName => { const baseFunc = Array.prototype[funcName] addHiddenProp(ObservableArray.prototype, funcName, function() { // 先調用 peek 方法,發送被觀察通知 // 然後在返回的數組項上調用方法 return baseFunc.apply(this.peek(), arguments) })})// ObservableArray.prototype.peekpeek(): T[] { // 發送被觀察通知 this.$mobx.atom.reportObserved() // 返回原始數組項供方法使用 return this.$mobx.dehanceValues(this.$mobx.values)}
對於會修改原數組的方法,調用 admin 對象上的方法進行操作,比如:
// ObservableArray.prototype.pushpush(...items: T[]): number { const adm = this.$mobx // spliceWithArray 方法內部會發送變化通知 adm.spliceWithArray(adm.values.length, 0, items) return adm.values.length}
Map
與數組的處理類似,Mobx 也實現了一個 ObservableMap 類,不過只支持字元串、數字或布爾值作為鍵。ObservableMap 在可觀察對象的基礎上,還要使鍵的增刪可觀察。它可以看做兩個可觀察映射和一個可觀察數組的組合:
這裡的可觀察映射指的是屬性值可觀察而對象屬性不可觀察,相當於 ObservableObjectAdministration 的 values 屬性。_data 存放鍵到可觀察值的映射,_hasMap 存放鍵是否存在的映射。_keys 屬性是一個 ObservableArray 實例,存放所有的鍵,從而使鍵的增刪可觀察。
三者配合從而得到一個可觀察粒度比對象更細的 Map。例如一個使用了 get 方法的 Derivation 只觀察該鍵值對的變化,而不會觀察其它鍵值對的設置和增刪:
const map = observable(new Map());autorun(() => { console.log(map.get(key));});map.set(key, value); // 新增 key-value 鍵值對,輸出 valuemap.set(key, anotherValue); // 修改為 key-anotherValue,輸出 anotherValuemap.set(prop, value); // 不輸出map.delete(prop); // 不輸出
來看 get 方法的實現:
get(key: string): V | undefined { key = "" + key // 如果存在該鍵值對,觸發該值的 reportObserved if (this.has(key)) return this.dehanceValue(this._data[key]!.get()) return this.dehanceValue(undefined)}has(key: string): boolean { if (!this.isValidKey(key)) return false key = "" + key // _hasMap 中對應值(observable(bool))發送觀察通知 if (this._hasMap[key]) return this._hasMap[key].get() return this._updateHasMapEntry(key, false).get()}
也就是說,這個 Derivation 只收集了這個鍵對應的值和表示是否存在該鍵的布爾值作為依賴,因而只對該鍵的值變化和該鍵值對的增刪作出響應。
再比如使用了 keys 方法的 Derivation 只觀察鍵值對的增刪,而使用了 values 方法的 Derivation 同時觀察鍵值對的增刪和值的變化:
keys(): string[] & Iterator<string> { // _keys.slice 觸發 _keys 的被觀察通知 return arrayAsIterator(this._keys.slice())}values(): V[] & Iterator<V> { // _keys.map 觸發 _keys 的被觀察通知 // 依次調用 this.get 觸發每一個可觀察值的被觀察通知 return (arrayAsIterator as any)(this._keys.map(this.get, this))}
發送被觀察通知
接下來看看 reportObserved 是如何發送通知的:
function reportObserved(observable: IObservable) { // 從全局狀態中取出當前正在跟蹤的 Derivation const derivation = globalState.trackingDerivation if (derivation !== null) { /** * 一處簡單的性能優化,如果是同一次 Derivation 執行訪問到 Observable * 直接忽略 */ if (derivation.runId !== observable.lastAccessedBy) { observable.lastAccessedBy = derivation.runId // 將 Observable 加入衍生的 newObserving 數組中, // 同時更新 unboundDepsCount(新增的依賴數量) derivation.newObserving![derivation.unboundDepsCount++] = observable } // 如果 Observable 不再有觀察者時,將他加入到一個全局隊列中待處理 } else if (observable.observers.length === 0) { queueForUnobservation(observable) }}
reportObserved 函數從全局狀態中取出當前正在執行的 Derivation,把 Observable 加入其 newObserving 數組中。Derivation 執行完後,會比較新舊 observing 數組,重新計算出依賴(這部分內容可以參看第四篇)。
發送變化通知
再來看 propageteChanged 是如何發送變化通知的:
function propagateChanged(observable: IObservable) { // 如果該 Observable 的所有觀察者都已經處於過期狀態,就沒有必要發送過期通知了 if (observable.lowestObserverState === IDerivationState.STALE) return // 標記所有觀察者都處於過期狀態 observable.lowestObserverState = IDerivationState.STALE // 遍歷所有觀察者,將它們的 dependenciesState 標誌設為過期,表示有依賴過期,需要重新計算 const observers = observable.observers let i = observers.length while (i--) { const d = observers[i] // 如果依賴由最新變為過期,調用該觀察者的 onBecomeStale 方法 if (d.dependenciesState === IDerivationState.UP_TO_DATE) d.onBecomeStale() d.dependenciesState = IDerivationState.STALE }}
可以看到,propagateChanged 只是將 Observable 和它的觀察者們的標誌設為了過期,並沒有實際執行任何的重新計算。在一個 Derivation 的依賴由最新變為過期時,會調用它的 onBecomeStale 方法。
Reaction 的 onBecomeStale 方法只是簡單的調用了 schedule 方法,將該 Reaction 的更新「加入了一個計劃表內」:
onBecomeStale() { this.schedule()}schedule() { // 已經在重新計算的計劃表內,直接返回 if (!this._isScheduled) { this._isScheduled = true // 該 Reaction 加入全局的待重新計算數組中 globalState.pendingReactions.push(this) runReactions() }}
這張計劃表實際上就是一個全局的數組,放置的是當前批次需要重新執行的所有 Reaction。
緊接著調用了 runReactions 函數:function runReactions() { // 此時處於事務中,inBatch > 0,會直接返回 if (globalState.inBatch > 0 || globalState.isRunningReactions) return reactionScheduler(runReactionsHelper)}
這種情形下 runReactions 實際不會執行 Reaction 的重新計算,因為此時至少會處於一個事務當中,即 Observable 在調用 reportChanged 時所開始的事務。
第四篇中我們將會看到,Reaction 是如何根據 Observalbe 的通知,動態更新自身的依賴或執行重新計算的。
推薦閱讀:
※Mobx 源碼解讀(四) Reaction
※如何評價數據流管理框架 MobX ?
※高性能 MobX 模式(part 2)- 響應變化
※Mobx 思想的實現原理,及與 Redux 對比