发布订阅模式又叫观察者模式,它定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会被通知。在js中,我们一般用事件模型来替代传统的发布订阅模式
- 我们可以订阅ajax的error,succ事件。或者在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧之后发布这个事件。
- 发布订阅模式可以取代对象之间硬编码的通知机制,一个对象不再显式的调用另一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要做任何修改,同样,发布者需要改变时,也不会影响之前的订阅者。
-
dom事件
比如我们订阅body上的click事件,当body被点击时,body节点便像订阅者发布这个消息。 -
自定义事件
如何实现发布订阅模式
- 首先指定谁是发布者
- 给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者
- 最后发布消息时,发布者会遍历缓存列表,依次触发里面存放的订阅者回调函数
let sales = {} // 定义一个售楼处作为发布者
sales.list = [] // 给这个发布者一个缓存队列存放订阅者的回调函数
sales.listen = (fn) => { this.list.push(fn) } 增加订阅者
sales.trigger = (...args) => { for(){ fn.apply(this, args) } } // 执行发布函数
至此我们实现了一个最简单的发布订阅模式,但是还有问题,我们接收到了发布者发布的每一个消息,如果我们只想接收自己感兴趣的消息怎么办。
let salesOffices = {}
salesOffices.list = {}
salesOffices.listen = (key, fn) => {
if (!this.list[key]) {
this.list[key] = []
}
this.list[key].push(fn)
}
salesOffices.trigger = (...args) => {
const key = args.shift()
if (!salesOffices.list[key] || !salesOffices.list[key].length) {
return false
}
for (let i=0, fn;fn = fn[i++]) {
fn.apply(this, ...args)
}
}
- 发布订阅模式的通用实现
如何让所有对象都拥有发布订阅功能
let event = {
clientList: {},
listen: (key, fn) => {
if (!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList.push(fn)
},
trigger: (...args) => {
let key = this.args.shift()
if (!this.clientList[key] || !this.clientList[key].length) { return false }
for (let i=0, fn;fn = this.clientList[key][i++])
}
}
const installEvent = (obj) => {
for (let name in event) { obj[name] = event[name] }
}
- 取消订阅的事件
event.remove = (key, fn) => {
const fns = this.clientList[key]
if (!fns) { return false } else if (!fn) {
fns && fns.length = 0;
} else {
for (let i=fns.length-1;i>=0;i--) {
if (fns[i] == fn) fns.splice(i, 1)
}
}
}
- 网站登录
我们现在有多个模块,它们渲染的前提有一个共同的条件,必须先异步请求获取用户的登录信息。当用户登录成功时,登录模块只需要发布登录成功的消息,登录模块并不关心业务方需要做什么
$.ajax('http://xxx.com', function(data) {
login.trigger('loginSucc', data) // 执行的时候触发所有的监听函数
})
let header = (() => {
login.listen('loginSucc', (data) => { header.set(data) })
return {
set: (data) => {}
}
})()
let nav = (() => {
login.listen('loginSucc', (data) => nav.set(data))
return { set: (data) => {} }
})()
- 全局的发布-订阅对象
我们可以使用一个全局的event对象来实现,订阅者不需要了解消息来自哪个发布者。 - 模块间的通信
- 必须先订阅再发布吗
我们所了解的都是订阅者必须先订阅消息,随后才能接收发布者发布的消息。在某些情况下,我们需要先将发布者发布的信息保存下来,等有对象来订阅的时候,再把消息发布给订阅者。
var event = (() => {
_listen = (key, fn, cache) => {
if (!cache[key]) { cache[key] = [] }
cache[key].push(fn)
}
_remove = (key, cache, fn) => {
if (cache[key] ){
if (fn) {
for (var i=cache[key].length;i>=0;i--) {
if (cache[key][i]==fn) cache[key].splice(i, 1)
}
} else { cache[key].length = 0 }
}
}
_trigger = () => {}
_create = (namespace) => {
}
return {
}
})()
-
命令模式
借助命令对象(command)的帮助,解开按钮和负责具体行为对象之间的耦合,用户并不知道command对象具体是一个什么对象。
命令对象让需要执行的对象和需要执行的方法隐藏,命令模式的接收者被当成命令对象的属性 -
撤销一系列指令
let Ryu = {
attack: () => {},
defense: () => {},
jump: () => {},
crouch: () => {}
}
- 宏命令
let MacroCommand = function() {
return {
commandList: [],
add: function(command) {
this.commandList.push(command)
},
excute: function() {
for (var i=0, command; command=this.commandList[i++]){
command.excute()
}
}
}
}
- 抽象类在组合模式中的作用
组合模式最大的优点就是可以一致的对待组合对象和基本对象。客户不需要知道当前处理的是宏命令还是普通命令,只要它是一个命令,并且有execute方法,就可以被添加到树中。
在java中,实现组合模式的关键是component类和leaf类都必须继承自component抽象类。这个component抽象类既代表组合对象,又代表叶对象。它能够保证组合对象和叶对象拥有同样的名字的方法,从而可以对同一消息做出反馈。组合对象和叶子对象的具体类型被隐藏在component抽象类后面。
针对compenent类来编写程序,客户操作的始终是Compenent对象,而不用区分到底是组合对象还是叶对象。所以我们往同一个对象里的add方法里,既可以添加组合对象,也可以添加叶对象。
public abstract class Component {
public void add (Component child) {}
public void remove(Component child) {}
}
public class Composite extends Component {
public void add (Component child) {}
public void remove (Component child) {}
}
public class Leaf extends Component {
public void
}
js的对象天生具有多态性
- 组合模式的例子-扫描文件夹
文件夹和文件之间的关系。
var Folder = function(name) {
this.name = name
this.files = []
}
Folder.prototype.add = function (file) {
this.files.push(file)
}
Folder.prototype.scan = () => {
conso;le.log(sao miao)
for (let i = 0, file; file = this.files[i++]) { file.scan() }
}
- 组合模式不是父子关系
- 对叶操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,就是ui一组叶对象的操作必须有一致性
- 引用父对象
组合对象保存了子节点的引用,此时树结构是从上至下的。
let Folder = function(name) {
this.name = name
this.parent = null
this.files=[]
}
Folder.prototype.add = (file) => {
file.parent = this
this.files.push(file)
}
Folder.prototype.scan = () => {
for (let i=0, file;file = this.files[i++]) {
file.scan()
}
}
Folder.prototype.remove = () => {
if (!this.parent) { return }
for (let files = this.parent.files, l=files.length-1;l>=0;l--) {
if (files[l] == this) { files.splice(l,1) }
}
}
- 模板方法模式
模板方法是一种基于继承的设计模式, 模板方法由两部分构成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构。
假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。在模板方法中,子类实现的相同部分被上移到父类中,而将不同的部分留待子类实现。
let Beverage = function() {}
Beverage.prototype.boilWater = function() {}
Beverage.prototype.brew = function(){}
Beverage.prototype.init = function() {
this.boilWater()
this.brew()
}
- 创建Coffee子类和Tea子类
饮料在这里是一个抽象的存在。接下来我们要创建咖啡类和茶类,并让它们继承饮料类。
let Coffee = function() {}
Coffee.prototype = new Beverage()
Coffee.prototype.brew = function() {}
Coffee.prototype.pourInCup = function() {}
Coffee.prototype.addCondiments = function() {}
let coffee = new Coffee()
coffee.init()
当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型都没有init方法,所以该请求会顺着原型链,被委托给Beverage原型上的init方法。
而init方法中已经规定好了泡饮料的顺序。
所以其中Beverage.prototype.init被称为模板方法,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
-
抽象类的作用
在java中,类分为两种,一种具体类,一种是抽象类。具体类可以被实例化,抽象类不能被实例化。如果有人写了抽象类,那么这些抽象类一定是用来被某些具体类继承的。
把对象的真正类型隐藏在抽象类或者接口之后,这些对象才可以被互相替换使用。除了用于向上转型,抽象类也可以表示契约。继承抽象类的所有子类都都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为了给子类定义公共接口。如果在子类中删掉了这些方法中的某一个,那么将不能通过编译器的检查。
比如如果在coffee子类中没有实现对应的brew方法,那么久无法得到咖啡。既然父类规定了子类的方法和顺序,子类基于应该拥有这些方法,并提供正确的实现。
js中没有抽象类,抽象类的一个作用是隐藏对象的具体类型。js不需要隐藏对象的类型。
当我们在js中使用原型继承模拟类式继承时,并没有编译器帮助我们进行类型的检查,我们也没办法保证子类会重写父类。 -
模板方法模式的使用场景
-
钩子方法
利用挂载到原型上的钩子方法判断公共方法是否被执行,钩子方法默认返回boolean值。 -
享元模式
使用享元模式的关键是区分外部状态和内部状态,是否能够被实例共享是划分的关键。可以被对象共享的属性划分为内部状态,而外部状态取决于具体的场景,并根据场景而变化。 -
享元模式的通用结构
上面的例子中,我们还有两个问题,一是通过构造函数new出了两个model对象,在其他系统中,也许并不是一开始就需要所有对象。二是给model对象手动设置了外部状态,在复杂系统中,外部状态可能相当复杂。
我们通过对象工厂解决第一个问题,只有某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器记录对象的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。 -
解决上传时的对象爆炸问题
假设有flash和插件两种上传方式,当用户上传时,都会调用window下的全局对象的js函数。
let id=0;
window.startUpload = (uploadType, files) => {
for (let i=0,file;file=files[i++];) {
let uploadObj = new Upload(uploadType, file.fileName, file.fileSize)
uploadObj.init(id++)
}
}
let Upload = function(uploadType, fileName, fileSize) {
this.uploadType = uploadType
this.fileName = fileName
this.fileSize = fileSize
this.dom = null
}
Upload.prototype.init = (id) => {
let that = this
this.id = id
this.dom = document.createElement('div')
this.dom.innerHTML = '...'
this.dom.querySelector('.delFile').onClick = () => {
that.delFile()
}
document.body.appendChild(this.dom)
}
Upload.prototype.delFile = () => {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('xxx')) { return this.dom.parentNode.removeChild(this.dom) }
}
- 享元模式重构文件上传
首先确定插件类型uploadType是内部状态,而fileName和fileSize是根据场景变化,每个文件fileName和fileSize都不一样,它们没办法被共享,它们只能被划分为外部状态。
- 剥离外部状态
let Upload = function(uploadType) {
this.uploadType = uploadType
}
Upload.prototype.delFile = function(id) {
UploadManager.setExternalState(id, this) // 把当前id对应的对象的fileSize值设置到共享对象中
...
}
在开始读取文件前,需要读取文件的实际大小,而文件的实际大小被存储在
setExternalState当中,所以需要通过setExternalState方法给共享对象设置正确的fileSize
接下来定义一个工厂来创建upload对象,如果某种内部状态对应的共享对象被创建过,那么直接返回这个对象,否则创建一个新对象:
let UploadFac = (function(){
let obj = {}
return {
create: function(uploadType) {
if (obj[uploadType]) { return obj[uploadType]}
return obj[uploadType] = new Upload(uploadType)
}
}
})()
- 管理器封装外部状态
管理器负责向UploadFac提交创建对象的请求,并用一个uploaddatabase保存所有upload对象的外部状态,以便在程序运行中给upload共享对象设置外部状态。
let uploadmanage = (() => {
let uploaddatabase = {}
return {
add: (id, uploadType, fileName, fileSize) => {
let weightObj = UploadFac.create(uploadType)
// 创建一个dom元素,绑定删除事件,再push到页面
let dom = ...
document.body.appendChild(dom)
uploaddatabase[id] = {
fileName,
fileSize,
dom
},
setExternalState: function(id, obj) {
给obj共享对象设置fileSize
let uploadData = uploaddatabase[id]
}
}
}
})()
-
没有内部状态的享元
在文件上传中,一般会提前检测来选择一种上传模式,如果浏览器支持插件就用插件,如果不支持就用flash,那么什么情况既需要插件又需要flash。
享元模式在需要的时候要将外部状态导入内部状态。 -
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是new,而是从池子里直接获取。如果对象池里没有空闲对象,则new一个,使用完成之后再进入池子等待被下次获取。
假设我们在开发一些地图应用,在搜索我家附近的时候,页面出现了2个小气泡。当我再搜索时,页面出现了6个小气泡。按照对象池的思想,第二次搜索开始前,我们只需要创建4个对象。
先定义一个获取小气泡节点的工厂,作为对象池的数组称为私有属性被包含在工厂闭包里,这个工厂有两个方法,create表示获取一个div节点,recover表示回收一个div节点。
网友评论