mobx源码分析(一) 构造响应式数据

mobx源码分析(一) 构造响应式数据

1. 思想实验

在开篇之初,我们先作一番思想实验。

视线折回到明代,水患侵扰了淳安县境,治理地方水务的官员把这情况上报给知县海瑞,海瑞又上报总督胡宗宪,而胡宗宪呢,他派出快马,向京师呈报奏章,吁请内阁早日筹备赈灾的粮食。与此同时,东厂番子早已把眼线布满全国,不等内阁向嘉靖奏报,嘉靖早已知晓了千里之外的动向。当我们把这环节牵涉到的各级官员设想为收发消息的节点,再把东厂番子设想为全局的监听器,我们就可以据此绘出一张图谱。

即如图谱中所见,node 负责接收消息 message,触发相应的动作 listener,再把消息传递给上层节点的同时,又会把消息传递给全局监听器 monitor,由 monitor 触发 reaction 行为。在现实中,reaction 常表现为命令 node 节点执行额外的动作,因此,在 node 节点向 monitor 上报消息的时候,也会携带当前 node 节点的信息。

当我们再类比到 mobx 的实现,绘制出如下图谱:

观察对象 observable 身兼多种能力。数据变更前,会执行 interceptor 拦截函数;数据变更后,执行 listener 监听函数,并将变更信息上报给全局监听器。在全局监听器中,startBatch, endBatch 用于开启、结束事务。事务执行过程中,将统计哪些观察者 observer 所观察的数据发生了变更、需要重新执行;在事务的尾端,通过 runAction 方法调用这些观察者的实际处理逻辑。

以上,我们需要探索的问题有:

  1. 怎样将数据转变成 observable?当数据变更时,observable 又怎样驱动 interceptor, listener 执行,并上报到全局环境?
  2. 怎样使 observer 订阅 observable?当数据变更时,使得相应的 observer 作出响应?

这篇文章致力于解答问题 1。下一篇文章将致力于解答问题 2。

2. 响应式数据

如果数据赋值这一动作委托给某函数处理,我们可以借助 AOP 编程思想,在该函数的执行过程中,添加一些附加操作,如上报变更信息、启用回调函数、或者触发指定事件等。正因为如此,Vue 才得以通过 Object.defineProperty 实现双向数据绑定;React 才得以通过 setState 实现组件重绘。mobx 融合了两种处理手法。首先,mobx 基于原始数据构建 observable 实例,在 observable 实例方法变更数据的过程中,将执行 interceptor, listener, reportChanged 等附加操作,这一机制如同 React 内建的 setState 方法在变更组件状态的同时,还能驱动组件重绘。其次,mobx 通过 Object.defineProperty 将原始数据(对象形式)的赋值、取值动作委托给 observable 实例方法去处理,使得原始数据的赋值动作转变成响应式的、取值动作又能获得原始数据内容,这一过程同 Vue 那样使用了 Object.defineProperty 方法。

mobx 能将如下几种数据类型转变为 observable 实例:ObservableValue 实例作为代理,能将基本数据类型的赋值动作转变成响应式的;ObservableObjectAdministration 实例能处理对象;ObservableArrayAdministration 实例能处理数组;ObservableMap 实例能处理 map 数据结构。

与此同时,mobx 使用 enhancer 递归地将层级较深的数据内容转变成上述四种 observable 实例,使得原始数据的子属性及元素(表现为数组、对象或 map)都具备响应式赋值能力。

2.1 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 {
  // 计算属性获取过程不允许变更响应式数据,严格模式下只能通过 action 改变响应式数据(即状态)
  checkIfStateModificationsAreAllowed(this)
  if (hasInterceptors(this)) {
      const change = interceptChange<IValueWillChange<T>>(this, {
          object: this,
          type: "update",
          newValue
      })
      if (!change) return UNCHANGED
      newValue = change.newValue
  }
  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()
  if (hasListeners(this)) {
    notifyListeners(this, {
        type: "update",
        object: this,
        newValue,
        oldValue
    })
  }
}

public get(): T {
  this.reportObserved()
  return this.dehanceValue(this.value)
}

以上代码,observableValue.set 方法具有几种职能:数据变更前,调用 interceptor 拦截器,拦截器可用于打断数据变更的动作;数据变更后,通过 reportChanged 方法将数据变更信息上报到全局监听器,并触发 listener 监听函数的执行。

其中,interceptor, listener 均挂载为 ObservableValue 的实例属性。

reportChanged 方法执行过程中,将把当前的 ObservableValue 标识为脏值,并驱动相应的 observer 作出响应。在 mobx 中,观察者 observer 基于数据是否变更作响应,而不是数据变更的内容,因此上报到全局环境并不需要携带数据变更的具体信息。

基于上述实现后,基本数据类型就可以把赋值动作委托给 observableValue.set 方法,以获得响应式变更数据的能力。

enhancer 用于将数据内容转变成 observable 实例,既能用于递归地处理复杂的数据结构,又能适应 js 弱类型语言的特征。比如,当把数据内容从基本数据类型切换成对象时,enhancer 就能使得对象属性的赋值动作也具有响应式效果。enhancer 分为两类,其一作为构造函数的参数,其二作为实例方法。

2.2 ObservableObjectAdministration

不同于基本数据类型的全量更新,对象单次只更新一个属性。ObservableObjectAdministration 实例和原始数据通过属性作桥接。无论 ObservableObjectAdministration 实例提供的 read, write, addObservableProp, addComputedProp 都在属性级别。顾名思义,read, write 方法用于针对属性的读写操作,addObservableProp 用于添加可观察属性,addComputedProp 用于添加计算属性。

除此而外,ObservableValue 实例不会改变原始数据 value 的读写过程。ObservableObjectAdministration 实例可通过 Object.defineProperty 方法改变原始数据的读写。

在 ObservableObjectAdministration 构造函数的实现内部,仰赖于 ObservableValue, ComputedValue 构造函数,因为其子属性既可能是 ObservableValue 可观察数据,ComputedValue 计算属性,又可能是普通数据。

read(key: string) {
  return this.values.get(key)!.get()
}

write(key: string, newValue) {
  const instance = this.target
  const observable = this.values.get(key)
  // 变更计算属性,予以报错处理,
  if (observable instanceof ComputedValue) {
    observable.set(newValue)
    return
  }

  if (hasInterceptors(this)) {
    const change = interceptChange<IObjectWillChange>(this, {
      type: "update",
      object: this.proxy || instance,
      name: key,
      newValue
    })
    if (!change) return
    newValue = (change as any).newValue
  }
  // 触发 ObservableValue 实例的 interceptor 拦截器, 通过 observableValue.enhance 获取终值
  newValue = (observable as any).prepareNewValue(newValue)

  if (newValue !== UNCHANGED) {
    const notify = hasListeners(this)
    const change =
      notify ? {
          type: "update",
          object: this.proxy || instance,
          oldValue: (observable as any).value,
          name: key,
          newValue
        }
        : null

    // 触发 ObservableValue 实例的 listener 订阅函数,并调用 observableValue.reportChanged 上报变更
    (observable as ObservableValue<any>).setNewValue(newValue)

    if (notify) notifyListeners(this, change)
  }
}

addObservableProp(propName: string, newValue, enhancer: IEnhancer<any> = this.defaultEnhancer) {
  const { target } = this
  // 校验 target 对象 propName 是否具有 configurable, writable 可能
  assertPropertyConfigurable(target, propName)

  if (hasInterceptors(this)) {
    const change = interceptChange<IObjectWillChange>(this, {
      object: this.proxy || target,
      name: propName,
      type: "add",
      newValue
    })
    if (!change) return
    newValue = (change as any).newValue
  }
  const observable = new ObservableValue(
    newValue,
    enhancer,
    `${this.name}.${propName}`,
    false
  )
  this.values.set(propName, observable)
  newValue = (observable as any).value

  Object.defineProperty(target, propName, generateObservablePropConfig(propName))
  this.notifyPropertyAddition(propName, newValue)
}

notifyPropertyAddition(key: string, newValue) {
  const notify = hasListeners(this)
  const change =
    notify 
      ? {
        type: "add",
        object: this.proxy || this.target,
        name: key,
        newValue
      }
      : null

  if (notify) notifyListeners(this, change)
  this.keysAtom.reportChanged()
}

以上代码,observableObjectAdministration.write 将依次调用挂载在 observableValue, observableObjectAdministration 实例上的 interceptor, listener,又通过 observableValue.reportChanged 上报到全局环境。在这里,作为属性值的 observableValue 实例颗粒度更细,通过 observableValue.reportChanged 上报到全局环境,将能使观察者 observer 对更细微的数据变动做出响应。为什么 observableObjectAdministration 没有实现 reportChanged 方法呢?因为,基于 enhancer,observableValue 所处理的数据有可能就是一个 observableObjectAdministration 实例,由 observableValue 处理上报动作是可行的。

observableObjectAdministration.addObservableProp 方法的实现中,既通过创建新的 ObservableValue 实例将属性变更上报到全局环境,又通过 keysAtom 属性(Atom 实例)上报上报到全局环境。前者使观察属性变更的 observer 作出响应,后者使观察属性集合的 observer 作出响应。

同 Vue,mobx 在取值时刷新 observer 和 observable 的依赖关系,通过 reportObserved 方法初始化依赖关系;在赋值时将 observable 标识为脏值,observer 需要作出响应。因此,单有 reportChanged 并不触发响应式动作,那时还没有 observer 需要基于数据变更作出响应;只有 reportObserved, reportChanged 同时存在,observer 才会作出响应。

2.3 ObservableArrayAdministration

与 Vue 相同,mobx 需要使数组的原型方法也具有响应式操作的能力。不同于 Vue 直接改写数组的实例方法,mobx 使用了 Proxy。

通过 Proxy 代理器,mobx 将对数组项的读取操作、push 等方法移交给 arrayExtensions 集合处理。因此,ObservableArrayAdministration 构造函数不同于 ObservableObjectAdministration,部分可以在 ObservableArrayAdministration 构造函数中实现的实例方法最终会在 arrayExtensions 对象中实现。

spliceWithArray(index: number, deleteCount?: number, newItems?: any[]): any[] {
  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 = EMPTY_ARRAY

  if (hasInterceptors(this)) {
      const change = interceptChange<IArrayWillSplice<any>>(this as any, {
          object: this.proxy as any,
          type: "splice",
          index,
          removedCount: deleteCount,
          added: newItems
      })
      if (!change) return EMPTY_ARRAY
      deleteCount = change.removedCount
      newItems = change.added
  }

  newItems = newItems.length === 0 ? newItems : newItems.map(v => this.enhancer(v, undefined))
  if (process.env.NODE_ENV !== "production") {
      const lengthDelta = newItems.length - deleteCount
      this.updateArrayLength(length, lengthDelta) // checks if internal array wasn't modified
  }
  const res = this.spliceItemsIntoValues(index, deleteCount, newItems)

  if (deleteCount !== 0 || newItems.length !== 0) this.notifyArraySplice(index, newItems, res)
  return this.dehanceValues(res)
}

spliceItemsIntoValues(index, deleteCount, newItems: any[]): any[] {
  if (newItems.length < MAX_SPLICE_SIZE) {
      return this.values.splice(index, deleteCount, ...newItems)
  } else {
      const res = this.values.slice(index, index + deleteCount)
      this.values = this.values
          .slice(0, index)
          .concat(newItems, this.values.slice(index + deleteCount))
      return res
  }
}

notifyArraySplice(index: number, added: any[], removed: any[]) {
  const notify = hasListeners(this)
  const change =
      notify 
          ? {
                object: this.proxy,
                type: "splice",
                index,
                removed,
                added,
                removedCount: removed.length,
                addedCount: added.length
            }
          : null

  this.atom.reportChanged()
  if (notify) notifyListeners(this, change)
}

notifyArrayChildUpdate(index: number, newValue: any, oldValue: any) {
  const notify = hasListeners(this)
  const change =
    notify 
      ? {
        object: this.proxy,
        type: "update",
        index,
        newValue,
        oldValue
      }
      : null

  this.atom.reportChanged()
  if (notify) notifyListeners(this, change)
}

以上代码,ObservableArrayAdministration 构造函数只提供了 spliceWithArray 方法,用于一次性变更数组项及数组的长度。在这一过程中,同样会驱动 interceptor, listener 执行,以及通过 reportChanged 上报到全局环境。

不同于 observableArrayAdministration 实例中变更数据的子属性由 observableValue 实例构成;observableArrayAdministration 实例中变更的数组项直接通过 enhancer 处理成 observable 实例。因为对于数组,观察者只订阅单个数组项变更的情况较少,不像对象需要监控每个属性的变更,两者监控的颗粒度不一样,前者就使用 enhancer 构造 observable 实例,后者通过 ObservableValue 构造 observable 实例。

const arrayExtensions = {
  clear(): any[] {
    return this.splice(0)
  },

  replace(newItems: any[]) {
    const adm: ObservableArrayAdministration = this[$mobx]
    return adm.spliceWithArray(0, adm.values.length, newItems)
  },

  splice(index: number, deleteCount?: number, ...newItems: any[]): any[] {
    const adm: ObservableArrayAdministration = this[$mobx]
    switch (arguments.length) {
      case 0:
        return []
      case 1:
        return adm.spliceWithArray(index)
      case 2:
        return adm.spliceWithArray(index, deleteCount)
    }
    return adm.spliceWithArray(index, deleteCount, newItems)
  },

  spliceWithArray(index: number, deleteCount?: number, newItems?: any[]): any[] {
    const adm: ObservableArrayAdministration = this[$mobx]
    return adm.spliceWithArray(index, deleteCount, newItems)
  },

  push(...items: any[]): number {
    const adm: ObservableArrayAdministration = this[$mobx]
    adm.spliceWithArray(adm.values.length, 0, items)
    return adm.values.length
  },

  pop() {
    return this.splice(Math.max(this[$mobx].values.length - 1, 0), 1)[0]
  },

  shift() {
    return this.splice(0, 1)[0]
  },

  unshift(...items: any[]): number {
    const adm = this[$mobx]
    adm.spliceWithArray(0, 0, items)
    return adm.values.length
  },

  reverse(): any[] {
    // 开发环境提示用户 reverse 方法不会改变观察数据的内容
    const clone = (<any>this).slice()
    return clone.reverse.apply(clone, arguments)
  },

  sort(compareFn?: (a: any, b: any) => number): any[] {
    // 开发环境提示用户 sort 方法不会改变观察数据的内容
    const clone = (<any>this).slice()
    return clone.sort.apply(clone, arguments)
  },

  remove(value: any): boolean {
    const adm: ObservableArrayAdministration = this[$mobx]
    const idx = adm.dehanceValues(adm.values).indexOf(value)
    if (idx > -1) {
      this.splice(idx, 1)
      return true
    }
    return false
  },

  get(index: number): any | undefined {
    const adm: ObservableArrayAdministration = this[$mobx]
    if (adm) {
      if (index < adm.values.length) {
        adm.atom.reportObserved()
        return adm.dehanceValue(adm.values[index])
      }
      // 提示用户超过数组长度
    }
    return undefined
  },

  set(index: number, newValue: any) {
    const adm: ObservableArrayAdministration = this[$mobx]
    const values = adm.values
    if (index < values.length) {
      checkIfStateModificationsAreAllowed(adm.atom)
      const oldValue = values[index]
      if (hasInterceptors(adm)) {
        const change = interceptChange<IArrayWillChange<any>>(adm as any, {
          type: "update",
          object: this as any,
          index,
          newValue
        })
        if (!change) return
        newValue = change.newValue
      }
      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 {
      // 超过数组长度,报错处理
    }
  }
};

[
  "every",
  "filter",
  "forEach",
  "indexOf",
  "join",
  "lastIndexOf",
  "map",
  "reduce",
  "reduceRight",
  "slice",
  "some",
  "toString",
  "toLocaleString"
].forEach(funcName => {
  arrayExtensions[funcName] = function () {
    const adm: ObservableArrayAdministration = this[$mobx]
    adm.atom.reportObserved()
    const res = adm.dehanceValues(adm.values)
    return res[funcName].apply(res, arguments)
  }
})

可以看出,arrayExtensions 对象封装了数组的原型方法,便于通过 Proxy 语法构建代理,为原始数据提供响应式的数组操作。

在 mobx 中,创建原始数组的代理通过 createObservableArray 函数实现。源码如下:

function createObservableArray<T>(
  initialValues: any[] | undefined,
  enhancer: IEnhancer<T>,
  name = "ObservableArray@" + getNextId(),
  owned = false
): IObservableArray<T> {
  const adm = new ObservableArrayAdministration(name, enhancer, owned)
  addHiddenFinalProp(adm.values, $mobx, adm)
  const proxy = new Proxy(adm.values, arrayTraps) as any
  adm.proxy = proxy
  if (initialValues && initialValues.length) {
    const prev = allowStateChangesStart(true)
    adm.spliceWithArray(0, 0, initialValues)
    allowStateChangesEnd(prev)
  }
  return proxy
}

const arrayTraps = {
  get(target, name) {
    if (name === $mobx) return target[$mobx]
    if (name === "length") return target[$mobx].getArrayLength()
    if (typeof name === "number") {
      return arrayExtensions.get.call(target, name)
    }
    if (typeof name === "string" && !isNaN(name as any)) {
      return arrayExtensions.get.call(target, parseInt(name))
    }
    if (arrayExtensions.hasOwnProperty(name)) {
      return arrayExtensions[name]
    }
    return target[name]
  },
  set(target, name, value): boolean {
    if (name === "length") {
      target[$mobx].setArrayLength(value)
      return true
    }
    if (typeof name === "number") {
      arrayExtensions.set.call(target, name, value)
      return true
    }
    if (!isNaN(name)) {
      arrayExtensions.set.call(target, parseInt(name), value)
      return true
    }
    return false
  },
  preventExtensions(target) {
    fail(`Observable arrays cannot be frozen`)
    return false
  }
}

通过创建代理对象,mobx 数组的处理效果就等同使用 Object.defineProperty 方法处理后的对象,赋值呈现响应式,取值获得更新后的值。

2.4 ObservableMap

mobx 介入对象处理的着眼点是属性,而对于 map 数据结构,其着眼点在于实例方法 set, get, has 等。因此,ObservableMap 与 ObservableArrayAdminstriation 处理上有相似的地方,都是对原始数据提供代理对象。在 ObservableArrayAdminstriation 的实现中,真正的代理对象需要在 createObservableArray 函数执行环节才能创建出 proxy 实例,而 ObservableMap 实例即是原始 map 结构的代理。

has(key: K): boolean {
  if (this._hasMap.has(key)) return this._hasMap.get(key)!.get()
  return this._updateHasMapEntry(key, false).get()
}

set(key: K, value: V) {
  const hasKey = this._has(key)
  if (hasInterceptors(this)) {
    const change = interceptChange<IMapWillChange<K, V>>(this, {
      type: hasKey ? "update" : "add",
      object: this,
      newValue: value,
      name: key
    })
    if (!change) return this
    value = change.newValue!
  }
  if (hasKey) {
    this._updateValue(key, value)
  } else {
    this._addValue(key, value)
  }
  return this
}

private _updateHasMapEntry(key: K, value: boolean): ObservableValue<boolean> {
  let entry = this._hasMap.get(key)
  if (entry) {
    entry.setNewValue(value)
  } else {
    entry = new ObservableValue(value, referenceEnhancer, `${this.name}.${key}?`, false)
    this._hasMap.set(key, entry)
  }
  return entry
}

private _updateValue(key: K, newValue: V | undefined) {
  const observable = this._data.get(key)!
  newValue = (observable as any).prepareNewValue(newValue) as V
  if (newValue !== UNCHANGED) {
    const notify = hasListeners(this)
    const change =
      notify 
        ? <IMapDidChange<K, V>>{
          type: "update",
          object: this,
          oldValue: (observable as any).value,
          name: key,
          newValue
        }
        : null
    observable.setNewValue(newValue as V)
    if (notify) notifyListeners(this, change)
  }
}

private _addValue(key: K, newValue: V) {
  checkIfStateModificationsAreAllowed(this._keysAtom)
  transaction(() => {
    const observable = new ObservableValue(
      newValue,
      this.enhancer,
      `${this.name}.${key}`,
      false
    )
    this._data.set(key, observable)
    newValue = (observable as any).value // value might have been changed
    this._updateHasMapEntry(key, true)
    this._keysAtom.reportChanged()
  })
  const notify = hasListeners(this)
  const change =
    notify 
      ? <IMapDidChange<K, V>>{
        type: "add",
        object: this,
        name: key,
        newValue
      }
      : null
  if (notify) notifyListeners(this, change)
}

上述代码中,除了常规的 interceptChange, notifyListeners, spyReportStart, spyReportEnd 以外,mobx 构建了两个 observable 实例集合,其一是 this._hasMap(由 ObservableValue 实例构成),其二是 this._data(由 enhancer 构建的 observable 实例构成)。其中,this._hasMap 用于实现 map.has 方法的响应式特征,其实际存储的值也是布尔类型;this._data 用于实现 map.set 方法的响应式特征,其存储的就是用户设置的数据。这两个 observable 实例集合可能同时会通过 reportChanged 方法上报数据变更(this.keysAtom 也会上报 map 集合所有的 key 键变化)。因此,在代码组织上,ObservableMap 构造函数使用了 mobx 内置的 tansaction 函数,用于在多次数据变更信息上报时协调事务,只有在最后一次数据上报时,才执行观察者 observer 的处理逻辑。

3 接口层

结合上图和前文,可得出 mobx 接口层的一些处理逻辑:

  1. 对于普通数据、map 数据结构的处理即是直接生成 ObservableValue, ObservableMap 实例。
  2. 对于数组,则通过 createObservableArray 创建 Proxy 代理实例,该代理实例的方法体中将调用 ObservableArrayAdminstrition 实例的响应式操作。
  3. 对于对象,则分为两种情况,当 options.proxy 为否值,只生成 ObservableObjectAdminstrition,再通过装饰器插入计算属性或可观察属性;当 options.proxy 为真值,创建 Proxy 代理实例,同样的,该代理实例的方法体中将调用 ObservableObjectAdminstrition 实例的响应式操作,再通过装饰器插入计算属性或可观察属性。
  4. 以上3点之外,mobx 通过 enhancer 决定对深层数据结构的处理操作,如 observable.deep 装饰器使用 deepEnhancer 增强器,将数据的深层结构统统转换为 observable 实例(默认操作);observable.shallow 装饰器使用 shallowEnhancer 增强器,只将数据的首层转换为 observable 实例;observable.ref 装饰器使用 referenceEnhancer 增强器,不会将数据转换为 observable 实例(除非新值就是个 observable 实例);observable.struct 装饰器使用 refStructEnhancer 增强器,同样不会将数据转换为 observable 实例,与 referenceEnhancer 区别是,当数据未作变更时,refStructEnhancer 将以引用对象形式返回原始数据,而不是值相同的新数据。

值得说明的是,mobx 处理对象时会批量添加计算属性或可观察属性,所以实现中使用 startBatch, endBatch 开启事务,使观察者的处理逻辑只执行一次。

编辑于 2018-08-16

文章被以下专栏收录