前言
本系列文章主要根据《JavaScript 设计模式与开发实践》整理而来,其中会加入了一些自己的思考。希望对大家有所帮助。
文章系列
js 设计模式 – 单例模式
js 设计模式 – 策略模式
js 设计模式 – 代理模式
js 设计模式 – 迭代器模式
概念
发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。
场景
DOM 事件
document.body.addEventListener(‘click’, function () {
alert(2);
}, false);
document.body.addEventListener(‘click’, function () {
alert(3);
}, false);
document.body.addEventListener(‘click’, function () {
alert(4);
}, false);
document.body.click(); // 模拟用户点击
优缺点
优点:发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。缺点:创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。
例子
销售处订阅房源
简单的发布订阅
var Event = function() {
this.list = []
}
Event.prototype.add = function(listener) {
this.list.push(listener)
}
Event.prototype.triggle = function() {
this.list.forEach(listener => {
listener()
})
}
var event = new Event()
event.add(()=>{console.log(‘ 房源 1 –80 平 –200 万 ’)})
event.add(()=>{console.log(‘ 房源 2 –200 平 –1000 万 ’)})
event.triggle()
或者
var event = {
list: [],
add(listener) {
this.list.push(listener)
},
triggle() {
this.list.forEach(listener => {
listener()
})
}
}
event.add(()=>{console.log(‘ 房源 1 –80 平 –200 万 ’)})
event.add(()=>{console.log(‘ 房源 2 –200 平 –1000 万 ’)})
event.triggle()
但这种不能区分是发不了什么消息,比如有两群人:订阅 80 平房源报价和订阅 200 瓶房源报价的人,这里两群人都会得到通知
改进
var event = {
list: {},
add(type, listener) {
if (!this.list[type]) {
this.list[type] = []
}
this.list[type].push(listener)
},
triggle(type) {
this.list[type] && this.list[type].forEach(listener => {
listener()
})
}
}
event.add(’80 平 ’, ()=>{console.log(‘ 房源 1 –80 平 –200 万 ’)})
event.add(’80 平 ’, ()=>{console.log(‘ 房源 2 –80 平 –300 万 ’)})
event.add(‘200 平 ’, ()=>{console.log(‘ 房源 2 –200 平 –1000 万 ’)})
event.triggle(’80 平 ’)
这里还少了一个取消订阅的功能
增加取消订阅
var event = {
list: {},
add(type, listener) {
if (!this.list[type]) {
this.list[type] = []
}
this.list[type].push(listener)
},
triggle(type) {
this.list[type] && this.list[type].forEach(listener => {
listener()
})
},
remove(type, fn) {
if (!this.list[type]) return
var index = this.list[type].findIndex(listener => listener === fn)
this.list[type].splice(index, 1)
}
}
var f1 = ()=>{console.log(‘ 房源 1 –80 平 –200 万 ’)}
var f2 = ()=>{console.log(‘ 房源 2 –80 平 –300 万 ’)}
var f3 = ()=>{console.log(‘ 房源 2 –200 平 –1000 万 ’)}
event.add(’80 平 ’, f1)
event.add(’80 平 ’, f2)
event.add(‘200 平 ’, f3)
event.remove(’80 平 ’, f2)
event.triggle(’80 平 ’) // 房源 1 –80 平 –200 万
上面代码结构还不是很清晰,我们再模拟销售部真实的场景
更真实的销售部场景
销售部
销售部有很多房源,如 80 平的,100 平的等
客户可以到销售部登记自己想买的房源面积,并留下姓名。到时候如果有房源,销售部就会通知客户
客户由于一些原因决定不买房的时候,可以取消订阅
客户
当有房源时,客户有一个接听报价的方法
var Event = function () {
this.list = {}
}
Event.prototype.add = function (area, client) {
if (!this.list[area]) this.list[area] = []
this.list[area].push(client)
}
Event.prototype.remove = function (area, client) {
if (!this.list[area]) return
var index = this.list[area].findIndex(item => item === client)
this.list[area].splice(index, 1)
}
Event.prototype.triggle = function (area, price) {
if (!this.list[area]) return
this.list[area].forEach(client => {
client.listen(area, price)
})
}
var Client = function (name) {
this.name = name
}
Client.prototype.listen = function (area, price) {
console.log(`${this.name} 收到 ${area} 平的房源报价 ${price}`)
}
var client1 = new Client(‘client1’)
var client2 = new Client(‘client2′)
var event = new Event()
event.add(’80 平 ’, client1)
event.add(‘100 平 ’, client1)
event.add(’80 平 ’, client2)
event.add(‘300 平 ’, client1)
event.remove(‘300 平 ’, client1)
event.triggle(’80 平 ’, 200) // client1 收到 80 平平的房源报价 200 client2 收到 80 平平的房源报价 200
event.triggle(‘100 平 ’, 500) // client1 收到 100 平平的房源报价 500
event.triggle(‘200 平 ’, 1000) //
event.triggle(‘300 平 ’, 1000) //
上面的代码虽然已经很好了,但是还是有一个缺点:订阅者接收不到订阅之前发布的消息,如下客户 3 也想订阅 80 平的房源,但他收不到任何消息
var client3 = new Client(‘client3′)
event.add(’80 平 ’, client3)
我们希望客户 3 也能收到消息
必须先订阅再发布吗
我们增加一个 cache 字段来记录历史房源报价
var Event = function () {
this.list = {}
this.cache = {}
}
Event.prototype.add = function (area, client) {
if (!this.list[area]) this.list[area] = []
this.list[area].push(client)
this.cache[area].forEach(price => {
client.listen(area, price)
})
}
Event.prototype.remove = function (area, client) {
if (!this.list[area]) return
var index = this.list[area].findIndex(item => item === client)
this.list[area].splice(index, 1)
}
Event.prototype.triggle = function (area, price) {
if (!this.cache[area]) this.cache[area] = []
this.cache[area].push(price)
if (!this.list[area]) return
this.list[area].forEach(client => {
client.listen(area, price)
})
}
var Client = function (name) {
this.name = name
}
Client.prototype.listen = function (area, price) {
console.log(`${this.name} 收到 ${area} 平的房源报价 ${price}`)
}
var client1 = new Client(‘client1’)
var client2 = new Client(‘client2′)
var event = new Event()
// event.add(’80 平 ’, client1)
// event.add(‘100 平 ’, client1)
// event.add(’80 平 ’, client2)
// event.add(‘300 平 ’, client1)
// event.remove(‘300 平 ’, client1)
event.triggle(’80 平 ’, 200) // client1 收到 80 平平的房源报价 200 client2 收到 80 平平的房源报价 200
event.triggle(‘100 平 ’, 500) // client1 收到 100 平平的房源报价 500
event.triggle(‘200 平 ’, 1000) //
event.triggle(‘300 平 ’, 1000) //
var client3 = new Client(‘client3′)
event.add(’80 平 ’, client3)
event.add(‘100 平 ’, client3)
网站登录
假如我们正在开发一个商城网站,网站里有 header 头部、nav 导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用 ajax 异步请求获取用户的登录信息。
这里留给读者自己实现