定义
发布—订阅模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。
代码实现
- 通过全局对象来把订阅者和发布者联系起来
// index.vue
<template>
<button @click="addVal">增加</button>
<div>{{val}}</div>
<button @click="remove">移除绑定</button>
</template>
<script lang="ts">
import event from '@/utils/event'
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const val = ref(0)
const fn = function(price = 1) {
val.value += price
}
event.listen('addval', fn)
return {
val,
fn
}
},
methods: {
addVal() {
event.trigger('addval',9)
},
remove() {
event.remove('addval', this.fn)
}
}
})
</script>
// event.js
class Event {
constructor() {
this.clientList = [];
}
listen(key, fn) {
if (!this.clientList[key]) this.clientList[key] = []
this.clientList[key].push(fn)
}
trigger() {
const key = Array.prototype.shift.call(arguments),
fns = this.clientList[key]
if (!fns || fns.length === 0) return false
for (let i = 0; i < fns.length; i++) {
fns[i].apply(this, arguments);
}
}
remove(key, fn) {
const fns = this.clientList[key]
if (!fns) return false
if (fn) {
fns && (fns.length = 0)
} else {
for (let i = 0; i < fns.length; i++) {
if (fns[i] === fn) fns.splice(i, 1)
}
}
}
}
export default new Event()
需求升级
- 增加命名空间
- 增加先发布后订阅的功能 😔 *以下代码功能不太完善还需优化,技术不精,暂未成功 *
// index.vue
<template>
<button @click="eventer">先发布后订阅</button>
</template>
<script lang="ts">
import eventer from '@/utils/eventer'
import { defineComponent, ref } from 'vue'
export default defineComponent({
methods: {
eventer() {
eventer.trigger('click', 1)
eventer.listen('click', function(a) {
console.log(a)
})
// 多次添加订阅,先发布后订阅的功能有问题,第一次以外的订阅都不会生效
eventer.listen('click', function(a) {
console.log(a + 1)
})
eventer.create('namespace1').listen('click', function(a) {
console.log(a, 'namespace1')
})
eventer.create('namespace1').trigger('click', 1)
eventer.create('namespace2').listen('click', function(a) {
console.log(a, 'namespace2')
})
eventer.create('namespace2').trigger('click', 2)
}
}
})
</script>
// eventer.js
export default Event = (function() {
var Event,
_default = 'default';
Event = function() {
var _listen,
_trigger,
_remove,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {}, // 命名空间缓存对象
_create,
each = function(ary, fn) {
var ret;
for (let i = 0; i < ary.length; i++) {
const n = ary[i]
ret = fn.call(n, i, n);
}
return ret
}
_listen = function(key, fn, cache) {
if (!cache[key]) cache[key] = []
cache[key].push(fn)
}
_remove = function(key, cache, fn) {
if (cache[key]) {
if (fn) {
for (let i = 0; i < cache[key].length; i++) {
if (cache[key][i] === fn) cache[key].splice(i, 1)
}
} else {
cache[key] = []
}
}
}
_trigger = function() {
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
stack = cache[key]
if (!stack || !stack.length) return
return each(stack, function() {
return this.apply(_self, args)
})
}
_create = function(namespace) {
var namespace = namespace || _default,
cache = {}, // 单事件缓存对象
offlineStack = [], // 单个event离线事件队列
ret = {
listen: function(key, fn, last) {
_listen(key, fn, cache) // 添加订阅
if (offlineStack === null) return
if (last === 'last') {
offlineStack.length && offlineStack.pop()()
} else {
each(offlineStack, function() {
this()
})
}
offlineStack = null
},
one: function(key, fn, last) {
_remove(key, cache)
this.listen(key, fn, last)
},
remove: function(key, fn) {
_remove(key, cache, fn)
},
trigger: function() {
var fn,
args,
_self = this;
_unshift.call(arguments, cache)
args = arguments;
fn = function() {
each(offlineStack, function(n, i) {
if (n === fn) offlineStack.splice(i, 1)
})
return _trigger.apply(_self, args)
}
if (offlineStack) return offlineStack.push(fn)
return fn()
}
}
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret
}
// 类似于代理模式,返回与真正单个event相同的接口
return {
create: _create,
one: function(key, fn, last) {
const event = this.create()
event.one(key, fn, last)
},
remove: function(key, fn) {
const event = this.create()
event.remove(key, fn)
},
listen: function(key, fn, last) {
const event = this.create()
event.listen(key, fn, last)
},
trigger: function() {
const event = this.create()
event.trigger.apply(this, arguments)
}
}
}()
return Event
})()
优缺点
- 优点:发布订阅模式可以做到时间上的解耦以及对象之间的解耦,这样即使在异步编程中我们也可以完成更松耦合的代码编写
- 缺点:创建订阅者需要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息到最后也未发生,但这个订阅者始终存在于内存中;另外过度使用时,对象与对象之间的必要联系会被深埋在背后,导致程序难以跟踪维护和理解。