MobX原理解析——响应式初始化
mobx是继redux之后又一大放异彩的状态管理库,相比于redux,其通过函数响应式编程的理念使得状态管理变得更加简单自由可拓展,避免了redux中大量的样板代码书写。本文就记录一下其核心功能的实现思路。
observable的初始化
mobx的使用通常从一个@observable
装饰器开始,实现一个类中属性的可观察化。该装饰器实际上是一个工厂函数createObservable如下。
function createObservable(v: any = undefined) {
if (typeof arguments[1] === "string") return deepDecorator.apply(null, arguments)
if (isObservable(v)) return v
const res = deepEnhancer(v, undefined, undefined)
if (res !== v) return res
return observable.box(v)
}
export const observable: IObservableFactory &
IObservableFactories & {
deep: {
struct<T>(initialValue?: T): T
}
ref: {
struct<T>(initialValue?: T): T
}
} = createObservable as any
可以看到,对于传入的非观察属性会先通过deepEnhancer函数处理。
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
}
该函数主要是针对对象,数组,Map进行处理,对于其他类型值则统一通过observable.box处理。
box
该方法简单的使用原属性构造了一个ObservableValue实例。源码如下。
box<T>(value?: T, name?: string): IObservableValue<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("box")
return new ObservableValue(value, deepEnhancer, name)
}
class ObservableValue {
// ...
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)
// ...
newValue = this.enhancer(newValue, this.value, this.name)
return this.value !== newValue ? newValue : UNCHANGED
}
setNewValue(newValue: T) {
const oldValue = this.value
this.value = newValue
this.reportChanged()
}
public get(): T {
this.reportObserved()
return this.dehanceValue(this.value)
}
}
其主要思路为实现get和set方法使得用户在使用该值时通过reportChanged和resportObserved方法通知其变化和被观察。
object
再看对象的可观察初始化。入口源码如下。
object<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
}
其核心也就是两个函数:asObservableObejct和extendObservable。先看前者,其功能就是给被观察的原对象创建一个新的代理对象作为可观察对象,源码将其命名为adm对象。进入到内部看其实现。
function asObservableObject(target, name?: string): ObservableObjectAdministration {
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()
const adm = new ObservableObjectAdministration(target, name)
addHiddenFinalProp(target, "$mobx", adm)
return adm
}
该对象实例化后会通过addHiddenFinalProp方法挂载到原对象的$mobx属性上并不可枚举。然后再通过extendObservable来创建代理对象的可观察属性。
function extendObservable<A extends Object, B extends Object>(
target: A,
...properties: B[]
): A & B {
return extendObservableHelper(target, deepEnhancer, properties) as any
}
function extendObservableHelper(
target: Object,
defaultEnhancer: IEnhancer<any>,
properties: Object[]
): Object {
const adm = asObservableObject(target)
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
const descriptor = Object.getOwnPropertyDescriptor(propSet, key)
defineObservablePropertyFromDescriptor(adm, key, descriptor, defaultEnhancer)
}
}
return target
}
其将属性转化为可观察的核心就在于defineObservablePropertyFromDescriptor方法中
function defineObservableProperty(
adm: ObservableObjectAdministration,
propName: string,
newValue,
enhancer: IEnhancer<any>
) {
// ...
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))
}
该方法中还是通过box中使用的ObservableValue将代理对象的属性定义为可观察,然后通过generateObservablePropConfig方法修改原对象访问器属性使其能访问到代理对象的可观察属性。该方法实现如下。
const observablePropertyConfigs = {}
function generateObservablePropConfig(propName) {
return (
observablePropertyConfigs[propName] ||
(observablePropertyConfigs[propName] = {
configurable: true,
enumerable: true,
get: function() {
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) {
observable.setNewValue(newValue)
}
}
array
数组本身也是对象,不过和对象不同的是,数组的属性名都是同样的索引,于是Mobx利用原型链的思路在其实现的类数组构造方法的原型上实现属性get和set的代理,其在初始化时就会完成这一步。源码如下。
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() {
return this.get(index)
},
set: function(value) {
this.set(index, value)
}
}
}
reserveArrayBuffer(1000)
// observable.array 工厂方法
array<T>(initialValues?: T[], name?: string): IObservableArray<T> {
if (arguments.length > 2) incorrectlyUsedAsDecorator("array")
return new ObservableArray(initialValues, deepEnhancer, name) as any
}
可以看到会添加1000个这样的属性,如果超过1000会通过reserveArrayBuffer再扩充。
再进入到ObservableArray看其可观察数组实现。
constructor(
initialValues: T[] | undefined,
enhancer: IEnhancer<T>,
name = "ObservableArray@" + getNextId(),
owned = false
) {
super()
const adm = new ObservableArrayAdministration<T>(name, enhancer, this as any, owned)
addHiddenFinalProp(this, "$mobx", adm)
if (initialValues && initialValues.length) {
this.spliceWithArray(0, 0, initialValues)
}
}
get(index: number): T | undefined {
const impl = <ObservableArrayAdministration<any>>this.$mobx
if (impl) {
if (index < impl.values.length) {
impl.atom.reportObserved()
return impl.dehanceValue(impl.values[index])
}
}
return undefined
}
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
adm.notifyArrayChildUpdate(index, newValue, oldValue)
}
} else if (index === values.length) {
adm.spliceWithArray(index, 0, [newValue])
} else {
// ...
}
}
结构和对象类似,同样实例化一个adm对象作为代理,再通过spliceWithArray方法将传入数组项添加到adm对象的values属性上并实现可观察。
为了保证完整截获类数组的变化并获取和原生数组一样的体验,和Vue类似的,ObservableArray同样实现了原生数组的方法并在其中插入了观察相关的逻辑。源码如下。
[
"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() {
return baseFunc.apply(this.peek(), arguments)
})
})
peek(): T[] {
this.$mobx.atom.reportObserved()
return this.$mobx.dehanceValues(this.$mobx.values)
}
map
和数组类似,对于Map类型mobx同样通过一个ObservableMap来管理其观察变化,并重写其原型方法。不过区别在于,Map还需要观察键的增删,于是其内部有三个属性管理可观察对象如下:
- _data: 存储键值对的可观察对象。
- _hasMap:存储键是否存在映射的可观察对象。
- _keys:存储所有键的可观察数组,并通过adm对象管理其变化。
三者配合可以根据Map的观测方式获取更细腻的响应。其观察实现和array类似,只是根据不同方法选择触发对应的可观察对象或数组的通知,本质就是可观察对象和数组的实现的综合,就不赘述。
分发通知
可观察属性完成初始化后,就需要被观察者收集并建立响应联系了。收集的实现需要其被观察时发送被观察的通知,响应的实现需要其变化时发送变化通知,也就是get和set内的reportObserved和propageteChanged了。
reportObserved
function reportObserved(observable: IObservable) {
const derivation = globalState.trackingDerivation
if (derivation !== null) {
if (derivation.runId !== observable.lastAccessedBy) {
observable.lastAccessedBy = derivation.runId
derivation.newObserving![derivation.unboundDepsCount++] = observable
}
} else if (observable.observers.length === 0) {
queueForUnobservation(observable)
}
}
可以看到收集依赖的核心在于从全局的trackingDerivation中取出当前正在跟踪的Derivation,然后将自己加入Derivation的依赖中。这部分的原理实际上是和Vue相当类似的。
propageteChanged
function propagateChanged(observable: IObservable) {
if (observable.lowestObserverState === IDerivationState.STALE) return
observable.lowestObserverState = IDerivationState.STALE
const observers = observable.observers
let i = observers.length
while (i--) {
const d = observers[i]
if (d.dependenciesState === IDerivationState.UP_TO_DATE) d.onBecomeStale()
d.dependenciesState = IDerivationState.STALE
}
}
变化通知也和Vue类似,本质就是将其依赖全部取出再执行。细节上稍微有些不同,mobx是将该属性的观察者标志为过期,过期的Derivation将会通过schedule方法将其更新计划放置到下一事务中,也就类似Vue的nextTick的实现了。实际上事务这一概念在React的状态更新中同样有着相似的职责。这里也可以看出这些视图框架的理念本质上差别也不大,学习也是一通百通的。
-- EOF --