最近公司在进行设计模式的分享,形成文章记录下来,同时也是对自己学习的总结,便于回顾,以及与大家交流。
定义
他定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖他的对象都会得到通知。
优势
- 使对象间松散的耦合在一起
- 时间结耦,无需关注什么时候发布
缺点
- 创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中
发布-订阅 VS 观察者
发布-订阅是一种一对多的关系,一个对象状态发生改变时,所订阅他的对象也会得到通知;
观察者模式,观察者与被观察者耦合的比较多,被观察者(Subject)中可以包含很多观察者(Observer),并且可以调用观察者中的函数,以此来通知观察者,是一个主动推送的过程,观察者得到推送进行相关更新。
观察者模式
代码实现
发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者” 的角色,把订阅者和发布者联系起来。
class Event {
constructor() {
// 缓存列表
this.clientList = {};
}
listen(key, fn) {
// 订阅的消息添加进缓存列表
(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, fn; fn = fns[i++]; ) {
// 依次trigger 同一个key的回调
fn.apply(this, arguments);
}
}
remove(key, fn) {
const fns = this.clientList[key];
if(!fns) {
// 如果key对应的消息没有被订阅 直接返回
return false;
}
if( !fn ) {
// 如果没有传回调函数
// 则remove所有事件
fns && (fns.length = 0);
}else{
// 匹配到对应的回调函数 则删除
fns.filter(item => item != fn);
}
}
}
const env = new Event();
env.listen('click', () => {
console.log('aaa');
})
env.trigger('click', 'aa');
env.remove('click');
env.trigger('click');
使用场景
这个模式在前端领域是非常普遍的,尤其是在vue、vuex中
- vue中的发布-订阅模式
// vue中$on方法
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 如果是数组,遍历为每个event绑定$on方法,
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
// $off
// 注销event,如果没有参数,注销所有event,如果只传方法名,则注销方法名对应的所有event
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all 如果不传参注销所有event
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// 如果event是数组则递归注销事件
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event];
// 本身不存在该事件 则直接返回
if (!cbs) {
return vm
}
// 如果只传了event参数,则注销该event下所有事件
if (!fn) {
vm._events[event] = null
return vm
}
// 如果传入了event以及fn,则遍历寻找对应的方法并删除
if (fn) {
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
}
return vm
}
- vuex中的发布-订阅模式
/* 存放订阅者 */
this._subscribers = [];
/* 注册一个订阅函数,返回取消订阅的函数 */
subscribe (fn) {
const subs = this._subscribers
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
/* 调用mutation的commit方法 */
commit (_type, _payload, _options) {
// check object-style commit
/* 校验参数 */
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
/* 取出type对应的mutation的方法 */
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
/* 执行mutation中的所有方法 */
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
/* 通知所有订阅者 */
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
- socket
- 订阅器、发布器
- 离线模式
必须先订阅后发布么?
有些情况下,需要先发布后订阅,比如QQ的离线消息,离线消息先缓存起来,等接受这上线之后,才可获取。