上一篇实现 MVVM 数据劫持是基于 ES5 的 Object.defineProperty() API,新的轮子将使用 Proxy 替代,follow Vue 3;在原有轮子的基础上做些改造,适配 Proxy

Object.defineProperty()Proxy 实现 MVVM 数据劫持的区别是:Object.defineProperty() 需要为每个 key 定义 getter 和 setter,也就是需要遍历 data 下的全部子属性;而 Proxy 只需要为 data 本身和内部嵌套的对象创建代理,每个对象统一代理访问内部属性,对外提供代理的引用。

MVVM Proxy 原理实现

效果(同上一个轮子):

index.htmlCompiler 模块不需要改动,详情见 上一篇

MVVM 模块

MVVM 初始化时需要代理 this.xxxthis.$data.xxx 或者 options.computed.xxx,所以给 this 创建一个 Proxy 代理,判断 keythis 本身、datamethodscomputed 中,分别通过 Reflect.get() 在对应的 object 上获取 key 值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
_proxyThis() {
    const { $options } = this
    const { computed } = $options
    return new Proxy(this, {
      get(target, key, receiver) {
        // 访问 MVVM 实例属性
        if (key in target) return Reflect.get(target, key, receiver)
        // 访问 data 属性
        if (key in $options.data)
          return Reflect.get(target.$data, key, receiver)
        // 访问 method
        if (key in $options.methods)
          return Reflect.get($options.methods, key, receiver)
        // 访问 computed 属性
        return typeof computed[key] === 'function'
          ? computed[key].call(target._vm)
          : Reflect.get(computed, key, receiver)
      },
      set(target, key, value, receiver) {
        // 设置 data 属性
        if (!target[key]) {
          return Reflect.set(target.$data, key, value, receiver)
        }
        return Reflect.set(target, key, value, receiver)
      },
    })
  }

new Proxy(this, handler) 创建的 this 代理需要作为 MVVM 构造函数的实例对象返回,因为提供给外部访问的是代理,即 this._vm

1
2
3
4
5
6
constructor(options = {}) {
  // ...
  this._vm = this._proxyThis()
	// ...
  return this._vm
}

然后将 methods 函数中的 this 绑定到 this._vm 上:

1
2
3
4
5
6
_bindMethods() {
  const methods = this.$options.methods
  Object.keys(methods).forEach(method => {
    methods[method] = methods[method].bind(this._vm)
  })
}

调用 Observeroptions.data 和嵌套对象创建代理:

1
this.$data = observe(options.data)

最后还是一样调用 Compiler 解析模板:

1
this.$compile = new Compiler(options.el || document.body, this._vm)

Dep 模块

Dep 模块存储结构跟上个轮子的 Dep 不同,因为使用 Proxy 代理后,只能为每个对象绑定一个 Dep 实例,一个 Dep 实例包含该对象内部所有属性的订阅者,而不是之前的一个属性一个 Dep 实例。

为了保证存储订阅者的数据结构的唯一性,使用 Set 结构保证订阅者不会重复;外层使用 Map 结构存储 key - Set 的映射关系。

订阅中心提供的每个接口,都需要 keysub 参数,根据 key 找出 Map 中映射的 Set,在订阅者 Set 中进行操作。

Observer 模块

Observer 模块使用 Proxy 代理 data 中嵌套对象的访问。

导出的 observe 函数递归遍历 data 内部属性,遇到对象就创建一个 Proxy 覆盖原始对象值,最后返回 data 对象的代理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export function observe(data) {
  // data 不是对象无法劫持,忽略
  if (!data || typeof data !== 'object') {
    return data
  }
  // 深度监听
  Object.keys(data).forEach(key => {
    data[key] = observe(data[key])
  })

  return Observer(data)
}

proxyObject 函数为 data 中的每个嵌套对象创建代理和绑定的 Dep 实例。跟上个轮子不同,在 getter 中,这里简化了添加订阅者的逻辑,如果判断 Dep.target 存在,直接通过闭包在绑定的 Dep 实例上把订阅该 keyWatcher 实例添加到订阅者中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const proxyObject = obj => {
  const dep = new Dep()
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 如果订阅者存在,直接添加订阅
      if (Dep.target) {
        dep.addSub(key, Dep.target)
      }
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, value, receiver) {
      if (Reflect.get(receiver, key) === value) {
        return
      }
      const res = Reflect.set(target, key, observe(value), receiver)
      dep.notify(key)
      return res
    },
  })
}

Watcher 模块

因为添加订阅者的逻辑已经在 Observer 模块做了,所以 Watcher 模块删除了 applyDep() 方法,其它不变:

1
2
3
4
5
6
// applyDep(dep) {
//   if (!this.depIds.hasOwnProperty(dep.id)) {
//     dep.addSub(this)
//     this.depIds[dep.id] = dep
//   }
// }

新的模块关系图

总结

Vue 3.0 使用了 Proxy 后,将会消除之前 Vue 2.x 中基于 Object.defineProperty 的实现所存在的一些限制:无法监听 属性的添加和删除数组索引值和长度的变更

GitHub 地址:https://github.com/Jancat/vue-mvvm-proxy

参考

https://github.com/xiaomuzhu/proxy-vue