发布订阅模式
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
DOM 上的 addEventListener 简单模拟
DOM(document object module)
及 document 对象,HTMLDocument 对象的一个实例
在控制台中输入,查看其原型链(继承关系):
null -- Object.prototype -- EventTarget.prototype -- Node.prototype -- Document.prototype -- HTMLDocument.prototype
其中 addEventListener 和 removeEventListener 在 EventTarget.prototype 对象上,所以可以在 DOM 元素上使用此方法
思路是对 onevent(onclick...)方法封装
1EventTarget.prototype.ion = function (event, callback) {2 // 如果没想错的话,DOM 应该有对应 `event`Callback 的一个栈,我不知道具体在哪,这里就先这样挂在实例上3 Array.isArray(this[`${event}Callback`]) || (this[`${event}Callback`] = [])45 this[`${event}Callback`].push(callback)67 this[`on${event}`] = function (e) {8 // call 保证了 callback 以 function 形式传入时,this 指向元素实例,9 // 以箭头函数传入时,call 失效,保证 this 指向原 context10 this[`${event}Callback`].forEach(cb => cb.call(this, e))11 }12}131415EventTarget.prototype.iremove = function (event, callback) {16 this[`${event}Callback`] = this[`${event}Callback`].filter(cb => cb !== callback)17}
Promise 中的发布订阅模式
这里只关注其中的发布订阅模式,不关注一些异步的处理和其他的方法,具体实现点我
1class Promise {2 constructor(executor) {3 const self = this4 self.state = 'pending'5 self.value = undefined6 self.reason = undefined7 // 存放成功态函数和失败态函数的两个栈8 self.onFulfilledCallbacks = []9 self.onRejectedCallbacks = []1011 function resolve(value) {12 if (self.state === 'pending') {13 setTimeout(() => { // 这里加上 setTimeout 是为了保证在 executor(异步) 执行后才执行14 self.state = 'fulfilled'15 self.value = value16 self.onFulfilledCallbacks.forEach(cb => cb(self.value)) // this.value 做下一个 then cb 的参数17 }, 0)18 }19 }2021 function reject(reason) {22 if (self.state === 'pending') {23 setTimeout(() => { // 同上24 self.state = 'rejected'25 self.reason = reason26 self.onRejectedCallbacks.forEach(cb => cb(self.reason))27 }, 0)28 }29 }3031 try {32 executor(resolve, reject)33 } catch (err) {34 reject(err)35 }36 }3738 then(onFulfilled, onRejected) {39 if (this.state === 'pending') {40 this.onFulfilledCallbacks.push(value => {41 this.value = onFulfilled(value) // 通过 this.value 把上一个 then cb 执行的结果传递为下一个 then cb 的参数42 })43 this.onRejectedCallbacks.push(reason => {44 this.reason = onRejected(reason)45 })46 }47 return this // 实现链式 then 调用48 }4950 static all(promises) {51 //52 }53}545556// test57const p = new Promise((resolve, reject) => {58 setTimeout(() => {59 resolve('ha')60 }, 1000)61})62p.then(r => {63 console.log(r)64 return 'ga'65}).then(r => {66 console.log(r)67})
以上只是很简单的一个实现,还很不完善
大体思路就是 then 方法给成功后和失败后的栈添加函数,相当于订阅,executer 执行中调用 resolve 或 reject,相当于发布,之后执行栈中的函数
Vue 中的发布订阅模式
Vue 源码中的 Dep
1class Dep {2 constructor() {3 this.subs = []4 }56 addSub(watcher) {7 this.subs.push(watcher)8 }910 notify() {11 this.subs.forEach(watcher => watcher.update())12 }13}
通过 addSub 方法添加 watcher 对象,由 notify 方法通知每个 watcher(订阅者),使其调用 update 方法,以此更新视图(watcher 监视可能会变化的节点,例:{{ }}、v-if... )
每个 Vue 实例都对应一个 Dep,所以其中的 subs 就是一个数组,而如果我们设计时,一个 Dep 可能观察多个对象,比如教务处人员会在学生挂科时通知他,这时让 sub 是一个对象
Vue 中一种简易的状态管理方式 eventBus(总线)
有时两个非父子组件的通信,如果项目不是很大,可以用 eventBus
1// eventBus.js2export default eventBus = {3 install(Vue, options) {4 Vue.prototype.$bus = new Vue()5 }6}78// app.js9import eventBus from '/eventBus.js'10Vue.use(eventBus)1112// NewTodoInput.vue13export default {14 methods: {15 addTodo() {16 eventBus.$emit('add-todo', { text: this.newTodoText })17 this.newTodoText = ''18 }19 }20}2122// DeleteTodoButton.vue23export default {24 methods: {25 deleteTodo(id) {26 eventBus.$emit('delete-todo', id)27 }28 }29}3031// Todos.vue32export default {33 created() {34 eventBus.$on('add-todo', this.addTodo)35 eventBus.$on('delete-todo', this.deleteTodo)36 },37 // 最好在组件销毁前38 // 清除事件监听39 beforeDestroy() {40 eventBus.$off('add-todo', this.addTodo)41 eventBus.$off('delete-todo', this.deleteTodo)42 },43 methods: {44 addTodo(newTodo) {45 this.todos.push(newTodo)46 },47 deleteTodo(todoId) {48 this.todos = this.todos.filter(function (todo) {49 return todo.id !== todoId50 })51 }52 }53}
必须先订阅后发布吗?
可以做一个队列,储存发布的消息,订阅之后执行队列就可以实现先发布后订阅了
JavaScript 中的发布订阅模式
其实传统 OOP 语言中的发布订阅模式与 Vue 中的比较像,是把订阅者传入到发布者中,Vue 源码由于代码量相对较大,用传统的模式会更易于维护,更加适合
而 addEventListener 则更贴近 JavaScript 的特性:高阶函数
优缺点
优点:
为时间上的解耦
对象之间的解耦
它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM,都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。
缺点:
创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。
发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug不是件轻松的事情。