观察者类实例化时,传入一个待观察的数据对象,构造器调用遍历方法来改写数据集中每一个键的get/set方法,在读取某个键的值时,将订阅者监听器(细节下一节讲)添加进回调队列,当set改变数据集中某个键的值时,调用观察者的notify( )方法找到对应键的回调队列并以此触发。
上面的代码可以应付一般情况,但存在一些明显的问题就是集中式的回调队列管理,subQueue实际上是一个HashMap结构:
subQueue = { 'myname':[fn1, fn2, fn3], 'otherAttr':[fn11,fn12, fn13], //... }不难看出这种管理回调的方式存在很多问题,遇到嵌套或重名结构就会出现覆盖,这个时候就不难理解Vue2.0源码中的做法了,在进行数据劫持时生成一个Dep实例,实例中维护一个回调队列用来管理发布订阅,当数据模型中的属性被set修改时,调用dep.notify( )方法来依次调用订阅者添加的回调,当属性被读取而触发get方法时,向dep实例中添加订阅者的回调函数即可。
2.2.6 发布订阅的连接截止目前为止,还有最后一个问题需要处理,就是订阅者实例sub和发布订阅管理器实例dep存在于两个不同的作用域里,那么要怎么通过调用dep.addSub(sub)来实现订阅动作呢?换个问法或许你就发现这个问题其实并不难回答,在SPA框架中,兄弟组件之间如何通信呢?通常都是借助数据上浮(公用数据提升到共同的父级组件中)或者EventBus来实现的。
这里的做法是一致的,在策略类中某个指令对应的处理方法中,当我们准备从数据模型this.data中读取对应的初值前,先将订阅者实例sub挂载到一个更高的层级(附件的demo中简单粗暴地挂载到全局,Vue2.0源码中挂载到Dep.target),然后再去读取this.data[expr],这个时候在expr属性被劫持的get方法中,不仅可以访问到属于自己的订阅管理器dep实例,也可以通过Dep.target访问到当前节点所对应的订阅者实例,那么完成对应的订阅逻辑就易如反掌了。
2.2.7 逻辑整合了解了上述细节,我们整理一下思路,整体看一下数据绑定所经历的各个环节:
2.2.8 Demo有关上面示例中d-model和d-click指令绑定的实现,本文不再赘述,笔者提供了包含详细注释的完整Demo,有需要的读者可以直接从附件中取用,最后Demo也会存放在我的github仓库。
2.2.9 Vue2.0中有关双向绑定的源码了解了上述细节,可以阅读《vue的双向绑定原理及实现》来看看 Vue2.0的源代码中是如何更加规范地实现双向数据绑定的。
2.3 数据劫持绑定存在的问题基于劫持的数据绑定方法是无法感知数组方法的,Vue2.0中使用了Hack的方法来实现对于数组元素的感知,其基本原理依旧是通过代理模式实现,在此直接给出源码Vue源码链接:
//Vue2.0中有关数组方法 const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // hack 以下几个函数 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 获得原生函数 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { // 调用原生函数 const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 触发更新 ob.dep.notify() return result }) })大致的思路是为Array.prototype上几个原生方法设置了访问代理,并将订阅管理器的消息发布方法混入其中,实现了对特定数组方法的监控。
三. 基于Proxy的数据绑定Vue官方已经确认3.0版本重构数据绑定代码,改为Proxy实现。