发布订阅者模式
设计模式和具体的开发语言无关,是广大开发者在开发实践过程当中总结出来的一种应用于特定场景的编码方式.公认的设计模式有23种,工厂模式,单例模式,装饰器模式,订阅发布模式…
vue双向绑定原理用到的是订阅发布模式
Vue使用观察者模式(又称发布-订阅模式)加数据劫持的方式实现数据响应式系统,劫持数据时使用 Object.defineProperty 方法将数据属性变成访问器属性。Object.defineProperty 是 ES5 中一个无法 shim 的特性,因此Vue 不支持 IE8 以及更低版本浏览器
简易的观察者模式模拟
let uid = 0
// 容器构造函数
function Dep() {
// 收集观察者的容器
this.subs = []
this.id = uid++
}
Dep.prototype = {
// 将当前观察者收集到容器中
addSub: function (sub) {
console.log(this.subs,sub);
this.subs.push(sub)
},
// 收集依赖,调用观察者的addDep方法
depend: function () {
if (Dep.target) {
Dep.target.addDep(this)
}
},
// 遍历执行容器中各观察者的run方法,以执行回调
notify: function() {
this.subs.forEach(sub => {
sub.run()
})
}
}
// 初始化当前观察者对象为空
Dep.target = null
// 数据劫持函数
function observe(data) {
// 防止重复对数据做劫持处理
if(data.__ob__) return
let keys = Object.keys(data)
keys.forEach(key => {
let val = data[key]
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return val
},
set: function (newValue) {
if((newValue !== newValue) || (newValue === val)){
return
} else {
val = newValue
dep.notify()
}
}
})
});
// 在被劫持的数据上定义一个不可遍历的内部属性
Object.defineProperty(data, '__ob__',{
configurable: true,
enumerable: false,
value: true,
writable: true
})
}
// 观察者构造函数
function Watcher(data, exp, callback) {
this.cb = callback
this.deps = {}
this.exp = exp
// 获取得到数据的函数
this.getter = this.parseExp(exp.trim())
this.data = data
this.value = this.get()
}
Watcher.prototype = {
run: function() {
let value = this.get()
let oldValue = this.value
if(value !== oldValue){
this.value = value
this.cb.call(null, value, oldValue)
}
},
addDep: function (dep) {
// 防止收集重复数据
if(!this.deps.hasOwnProperty(dep.id)){
dep.addSub(this)
this.deps[dep.id] = dep
}
},
get: function () {
// 将实例对象变为当前观察者对象
Dep.target = this
// 读取数据,从而触发数据get方法
let value = this.getter.call(this.data, this.data)
// 依赖收集完毕,当前观察者对象置为空
Dep.target = null
return value
},
// 通过形如‘a.b’的字符串形式获取数据值
parseExp: function(exp) {
if(/[^\w.$]/.test(exp)) return
let exps = exp.split('.')
return function(obj) {
return obj[exps[1]]
}
}
}
// 监测函数
function $watch(data, exp, cb) {
observe(data)
new Watcher(data, exp, cb)
}
let a = {
b: 100,
c: 200
}
const callback = function(newValue, oldValue) {
console.log(`新值为:${newValue},旧值为:${oldValue}`)
}
$watch(a, 'a.b', callback)
$watch(a, 'a.c', callback)
a.b = 101
a.c = 201
逻辑结构图
响应式系统可以分为三个阶段:数据劫持(图中蓝线表示)、收集依赖(图中红线表示)、触发依赖(图中绿线表示)。
- 数据劫持
在数据劫持函数 observe 中,首先检测对象中是否存在不可遍历的属性 ob 。如果存在,则表示该对象已经转化为响应式的;如果不存在,在数据转化之后添加上 ob 属性。
然后循环遍历对象的属性,将数据属性变成访问器属性,每个访问器属性通过闭包引用一个 Dep 实例 dep 。在读取属性时,会触发 get 方法,然后调用 dep.depend() ,收集依赖。在为属性设置新值时,会触发 set 方法,然后调用 dep.notify() ,触发依赖。通过 observe 方法仅仅是改造对象属性,对象属性的 get 和 set 此时并没有触发。
- 收集依赖
通过 Watcher 函数为响应式数据添加依赖。所谓依赖,是指当数据变更时需要触发的回调函数。
Watcher 函数实例化时通过调用 get() 方法先将实例对象设置成当前观察者对象,然后读取数据,数据的 get 方法被调用,接着调用数据闭包引用的数据 dep 的 depend() 方法。
在 depend() 中,会将 dep 传入当前观察者的 addDep() 方法。在 addDep() 方法中,首先防止重复收集依赖,然后调用 dep.addSub() 方法将当前观察者添加到 dep 的subs 属性中,完成依赖的收集。
- 触发依赖
触发依赖是指在数据发生改变时,将数据闭包引用的变量 dep 中存储的观察者对象的回调依次执行。
当数据改变时,会调用 set 方法,然后执行 dep.notify() 方法,该方法遍历数组 dep.subs ,执行数组中每个观察者对象的 run() 方法。Watcher 实例的 run() 方法将数据改变前、改变后的值传入回调函数中执行,完成依赖的触发。
异步执行
在触发依赖的过程中调用 update() 方法,该方法有三种情况:作为计算属性的观察者、指明要同步执行、默认异步执行。
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
当选项 sync 为 true 时,直接通过执行 run() 方法直接调用回调函数。而选项 sync 除非明确指定,默认是 false ,也就是说,默认对依赖的触发是异步执行的。
异步执行主要是为了优化性能,例如当模板中的数据改变时,渲染函数会重新求值,完成重新渲染。如果同步执行,每次修改一个值都要重新渲染,在复杂的业务场景下可能会同时修改很多数据,多次渲染会导致很严重的性能问题。
如果异步执行,每次修改属性的值之后并没有立即重新求值,而是将需要执行更新操作的观察者放入一个队列中,队列中没有重复的观察者对象,这样就能达到优化性能的目的。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) { i-- }
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
queueWatcher() 方法首先判断观察者队列中是否含有要添加的观察者,如果已经存在,则不做任何操作。flushing 变量为队列是否正在执行更新的标志,如果队列没有执行则直接将观察者对象存到队列中,如果队列正在执行更新,则需要保证观察者的执行顺序。
变量 waiting 初始值为 false,在执行 if 判断之后变为 true ,也就是说 nextTick(flushSchedulerQueue) 只会执行一次。nextTick() 方法比较复杂,在这里可以简单理解成与 setTimeout(fn, 0) 功能相同,作用就是在下一事件循环开始时立即调用 flushSchedulerQueue ,从而将队列中的观察者统一执行更新。
总结
Vue数据响应式系统总体来说分为两步:1、劫持数据;2、将数据改变要触发的回调函数与数据关联起来。
observe 函数的功能就是劫持数据,让数据在被读取时收集依赖,在改变时触发依赖。如果数据为纯对象,则将其属性转变成访问器属性;如果数据是数组类型,则通过重写能够改变自身的方法来实现。对象添加、删除属性以及数组通过直接赋值的方式改变并不会触发依赖,这时要使用 Vue.set()、Vue.delete() 方法来操作。
Dep 函数是用来生成盛放依赖的容器。收集依赖最终都是收集到其实例对象的 subs 数组属性中,触发依赖最终操作时遍历执行 subs 中的观察者对象上的回调函数。
Watcher 函数主要是将被观察的数据与数据改变后要执行的回调函数关联起来。为了提升性能,在这个过程中要避免数据收集的依赖有重复;当数据改变时要异步执行更新。