vue 最核心的卖点是数据驱动和组件。浏览器原生提供的交互是通过dom api来修改dom元素,由于浏览器兼容性问题后面的框架如jquery对原生的api进行了一层的封装以屏蔽浏览器的差异性,但并未作出实质的改变。想想这个过程,通常是数据发生变化,js根据变化的情况进行判断而后操作dom。dom变动的本质实际根本上实际是由数据驱动,我在第一家公司数字政通(egova)首次接触了的此类框架knockout
。
所谓数据驱动其实就是监听数据发生变化,当数据发生变化后通知订阅者做出响应。
在介绍v2.6.11实现之前,我们先看下观察者模式。
观察者模式
一般的观察者模式只存在订阅关系,是单向的,即主题保存着观察者的引用,为了和vue实现的对齐,下面的实现添加另一层关系即观察者(Observer
或Watcher
)对于主题(Subject
)的依赖关系(因此主题也可以认为是依赖Dep
)。 此时观察者和主题的关系则变为双向的,并且是多对多的,即一个主题可以被多个观察者订阅,一个观察者也可以依赖多个主题(有多个依赖)。
主题/依赖:Dep
let uid = 0;
export default class Dep {constructor() {this.watchers = [];this.id = `Dep_${uid++}`;}addSub(watcher) {if (this.watchers.findIndex(item => item === watcher) >= 0) {return}this.watchers.push(watcher);}notify() {this.watchers.forEach(watcher => watcher.update(this))}
}
观察者:Watcher
let uid = 0;
export default class Watcher {constructor() {this.deps = []this.id = `Watcher_${uid++}`;}addDep(dep) {if (this.deps.findIndex(item => item === dep) >= 0) {return}this.deps.push(dep)dep.addSub(this);}update(dep) {console.log(`${this.id}_${dep.id}, changed`)}
}
test
import Watcher from "./Watcher.js";
import Dep from "./Dep.js";
function main() {const watcherOne = new Watcher();const watcherTwo = new Watcher();const subjectOne = new Dep();watcherOne.addDep(subjectOne)watcherTwo.addDep(subjectOne)subjectOne.notify();console.log('----------');const subjectTwo = new Dep();watcherTwo.addDep(subjectTwo)subjectTwo.notify();
}
main();

小结
vue的数据驱动视图的核心就是,数据具备响应式能力(即上面观察者模式中的主题的能力可以被订阅-addSub
,也可以通知变更-notify
)
另外上面观察者和主题的双向关系:订阅关系和依赖关系(依赖收集就是指的依赖关系的建立)是开发者手动建立的。但是在vue开发中,我们并未做过这类事情,这个已经包含在框架内了,框架会自动进行依赖收集(addSub/addDep)和派发更新(notify)
通过Proxy
和Object.definePropery
两种方式来拦截数据的读取-getter和修改-setter,vue-v2.x版本的响应式能力基于Object.definePropery
实现。
如何自动收集依赖?依赖关系建立的发起人是观察者,可以设置一个全局的变量来记录当前观察者(那么自然要求依赖收集需要时同步的过程),这个观察者是有“作用过程”的,在这个“作用过程”中读取了响应式数据即进入响应式数据的getter,在getter中建立双向关系。
建立完双向关系后,派发更新就简单了,直接在响应式数据的setter中通知所有的观察者
由于数据需要具备addSub
和notify
能力,后面的实现(也是vue的实现)将这些能力收敛到Dep
类中,将数据和Dep
关联起来(一对一),自然就将二者捆绑起来了。这里关联在vue中有两种不同的实现(一个是对象属性,一个是闭包),分别针对不同的场景,后面会看到。
下面我们看下依赖收集和派发更新的具体实现,vue中常使用的响应式数据为普通对象和数组两种形式,下面我们只以普通对象来说明这两个问题(数组后面单独再补充)。
响应式的实现(v2.x)
1. 数据增强(具备响应式能力)
observe
Object.isExtensible
判断对象是否可扩展性> 默认情况下,对象都是可以扩展的,即对象可以添加新的属性和方法。使用Object.preventExtensions()、Object.seal()和Object.freeze()方法都可以标记对象为不可扩展。
observe
方法用来作为增强value
的入口,判断是否可以进行增强(具备响应式能力)
1.我们这里由于是使用普通对象作为案例,因此先判断是否是普通对象,如果不是则忽略
2.然后会再判断value
是否已经是响应式对象了,如果是则直接返回
3.即是可扩展的普通对象又不是响应式对象,则进行增强:new Observer()。
function isPlainObject(obj) {return obj.toString() === '[object Object]'
}
export function observe(value) {if (!isPlainObject(value)) {return}let obif (value.__ob__ instanceof Observer) {ob = value.__ob__} else