VUE源码的观察者模式解析
源码地址:https://github.com/vuejs/vue/blob/dev/src/core/observer/dep.js
https://github.com/vuejs/vue/blob/dev/src/core/observer/watcher.js
1.观察者模式
观察者模式(Observer):通常又被称作为发布-订阅者模式。它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。观察者模式的使用场景如下:
a.当一个抽象模型有两个方面,其中一个方面依赖于另一方面。讲这两者封装在独立的对象中可以让它们可以各自独立的改变和复用
b.当一个对象的改变的时候,需要同时改变其它对象,但是却不知道具体多少对象有待改变
c.当一个对象必须通知其它对象,但是却不知道具体对象到底是谁。换句话说,你不希望这些对象是紧密耦合的
2.VUE源码中观察者模式的使用
vue源码中数据初始化内容应用了观察者模式,具体过程如下图:
由上图可以发现,整个观察者模式中,发布者实际上是通过数据劫持实现的,当数据发生改变的时候,数据劫持之后向订阅者发送劫持信息,消息中转站(Dep)接收到信息再转发给订阅者,订阅者接收消息之后,将数据变更的消息进行传递,传递到render()实现数据的更新。
3.具体实现过程
A.数据劫持
Vue对data里的数据都进行了绑定(劫持),那么vue就要时刻保持着对对象的遍历从而将监控属性是否发生了改变。源码如下:
walk (obj: Object) { const keys = Object.keys(obj) // 遍历将其变成 vue 的访问器属性 for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) } }
// 设置为访问器属性,并在其 getter 和 setter 函数中,使用发布/订阅模式,互相监听。 export function defineReactive ( obj: Object, key: string, val: any ) { // 这里用到了观察者(发布/订阅)模式进行了劫持封装,它定义了一种一对多的关系,让多个观察者监听一个主题对象,这个主题对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。 // 实例化一个主题对象,对象中有空的观察者列表 const dep = new Dep() // 获取属性描述符对象(更多的为了 computed 里面的自定义 get 和 set 进行的设计) const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } const getter = property && property.get const setter = property && property.set let childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // 收集依赖,建立一对多的的关系,让多个观察者监听当前主题对象 get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() // 这里是对数组进行劫持 if (Array.isArray(value)) { dependArray(value) } } } return value }, // 劫持到数据变更,并发布消息进行通知 set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = observe(newVal) dep.notify() } }) }
B.消息封装实现中转站
我们在劫持到数据变更的时候,并进行数据变更通知的时候,如果不做一个”中转站”的话,我们根本不知道到底谁订阅了消息,具体有多少对象订阅了消息。发布者A与订阅者B进行信息传递,两人都知道对方这么一个人的存在,但A不知道具体B是谁以及到底有多少订阅者订阅着自己,可能很多订阅者都订阅着A的信息, 发布者A 需要通过暗号 收集到所有订阅着其消息的订阅者,这里对于订阅者的收集其实就是一层封装。然后A只需将消息发布出去,而订阅者们接受到通知,只管进行自己的 update 操作即可。Dep需要完成两个内容,1.定义subs数组,用来收集订阅者Watcher;2.当劫持到数据变更的时候,通知订阅者Watcher进行update操作,代码如下:
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { // 用来给每个订阅者 Watcher 做唯一标识符,防止重复收集 this.id = uid++ // 定义subs数组,用来做依赖收集(收集所有的订阅者 Watcher) this.subs = [] } // 收集订阅者 addSub (sub: Watcher) { this.subs.push(sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
Watcher负责做的事情就是订阅Dep ,当Dep发出消息传递的时候,所以订阅着Dep的Watcher会进行自己的Update操作
export default class Watcher { vm: Component; expression: string; cb: Function; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm vm._watchers.push(this) this.cb = cb // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 解析表达式 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} } } this.value = this.get() } get () { // 将目标收集到目标栈 pushTarget(this) const vm = this.vm let value = this.getter.call(vm, vm) // 删除目标 popTarget() return value } // 订阅 Dep,同时让 Dep 知道自己订阅着它 addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { // 收集订阅者 dep.addSub(this) } } } // 订阅者'消费'动作,当接收到变更时则会执行 update () { this.run() } run () { const value = this.get() const oldValue = this.value this.value = value this.cb.call(this.vm, value, oldValue) } }