在 ES6 把 Promise 写进语言标准前,为实现异步编程,经常会采用 观察者模式(发布-订阅模式) 作为替代传递回调函数的方案。
它定义了一种一对多的关系,让多个观察者订阅同一主题对象,当主题状态发生改变则立即发布,通知所有的订阅者。发布者和订阅者之间完全解耦,仅仅是共享同一自定义事件的名称。当新的订阅者出现,发布者无需做任何修改,反之亦然。
常见的需要观察者模式的场景:
在任意一个需要登录的网站中,header,navbar,消息列表,购物车等模块的渲染,都需要登陆后拿到用户信息。但是ajax登录请求完成的时间无法确定,如果在ajax回调中调用各模块的方法来更新用户信息的话,耦合性太强,新增/修改模块的成本太高,业务模块更复杂的时候很难维护。这个时候需要的就是观察者模式。
Vue 在实现数据绑定时也采用的观察者模式来实现数据的订阅,订阅者维护每一次更新之前的数据,当数据发生变化,订阅者将执行自身设定的回调逻辑,并更新所维护数据的值。
实现观察者模式:
- 指定发布者
- 给发布者添加一个缓存列表,用于存放回调函数以通知订阅者
- 发布消息时,遍历缓存列表,触发每一个订阅者回调函数
并且除了缓存列表之外,还需要订阅,发布,取消订阅这三个方法。
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 28 29 30 31 32 33 34 35 36 37 38 39
| var event = { clientList: [], listen: function(key, fn){ if (!this.clientList[key]) { this.clientList[key] = [] } this.clientList[key].push(fn) }, trigger: function () { var key = Array.prototype.shift.call(arguments) var fns = this.clientList[key] if (!fns || fns.length === 0) return false for (var i = 0, fn; fn = fns[ i++ ];){ fn.apply(this, arguments) } }, remove: function (key, fn) { var fns = this.clientList[key] if (!fns){ return false; } if (!fn){ fns && (fns.length = 0) } else { for (var l = fns.length - 1; l >=0; l--){ var _fn = fns[l] if (_fn === fn){ fns.splice(l, 1) } } } } }
|
这就是观察者模式的一个通用实现。
在实际场景中,有可能需要多个发布者对象,需要多个类似上面 event 对象的绑定,非常麻烦,发布订阅也并没有完全解耦,需要知道这个对象的名字。因此也可以采用 全局的 Event 对象 来实现。
同时,也有可能为了避免命名冲突,需要 创建命名空间 ,或者是由于为了实现可以先发布再订阅,创建离线堆栈 等等,可定制高级版的观察者模式。
采用观察者模式需要注意的问题:
- 实现观察者模式本身需要耗费内存,如果发布并不常发生,而订阅却始终存在于内存中,造成了一定程度的浪费
- 由于模块之间的联系由具体的耦合转为抽象,因此过多使用观察者模式的话,模块关系很难追溯,代码也很难维护
Vue源码中的使用:
Vue 实现数据绑定依靠的是 Object.defineProperty() 的自定义getter/setter 来进行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export default class Dep{ constructor(){ this.subs = [] } addSub(sub){ this.subs.push(sub) } notify(){ this.subs.forEach((sub) => sub.update()) } }
|
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 28 29 30 31 32 33
| import Dep from 'Dep' export default class Watcher{ constructor(vm, expOrFn, cb){ this.vm = vm this.cb = cb this.expOrFn = expOrFn this.val = this.get() } update(){ this.run() } run(){ const val = this.get() if(val !== this.val){ this.val = val; this.cb.call(this.vm) } } get(){ Dep.target = this const val = this.vm._data[this.expOrFn] Dep.target = null return val; } }
|
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| import Dep from 'Dep' export default class Observer{ constructor(value){ this.value = value this.walk(value) } walk(value){ Object.keys(value).forEach(key => this.convert(key, value[key])) } convert(key, val){ defineReactive(this.value, key, val) } } export function defineReactive(obj, key, val){ var dep = new Dep() var chlidOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: ()=> { console.log('get value') if(Dep.target){ dep.addSub(Dep.target) } return val }, set: (newVal) => { console.log('new value seted') if(val === newVal) return val = newVal chlidOb = observe(newVal) dep.notify() } }) } export function observe(value){ if(!value || typeof value !== 'object'){ return } return new Observer(value) }
|
在有些文章中,观察者模式与发布/订阅模式还有些差别,可以观摩这篇 ObserverPattern去学习一下