前言
设计模式是开发中很重要的内容,我更愿意把他理解为一种指导思想。按照这种指导思想去解决问题,代码可以更低的耦合更高的内聚并且更健壮和逻辑清晰。
单例模式
一个类只有一个实例,并提供一个访问它的全局访问点。单例模式顾名思义保证一个类仅有一个实例,并且提供一个访问它的全局访问点。单例的好处是可以减少不必要的开销例如页面需要展示一个弹框,那么这个弹框只在首次的时候会进行创建,之后在进行点击的时候使用的都是之前创建好的。例如登录框单例,vue中的store,router
单例模式实现
核心思想为:createDiv方法只负责创建div,扩展方法交给原型链上的init。创建单例时使用立即执行函数和闭包
进行封装
var CreateDiv = function(html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function() {
var div = document.createElement('div')
div.innerHTML = this.html
document.appendChild(div)
}
var ProxysingletonCreateDiv = (function() {
var instance
return function(html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
var a = new ProxysingletonCreateDiv('test1')
var b = new ProxysingletonCreateDiv('test2')
alert(a === b) // true
适用于js的惰性单例
控制只有一个对象的操作抽象出来是个什么样子:
var obj
if (!obj) {
obj = xxx
}
于是就可以把这个操作的逻辑封装到一个getSingle
函数中,然后把要执行的函数当作参数传入进去:
var getSingle = function(fn) {
var result
return function() {
result || (result = fn.apply(this, arguments))
}
}
var createLoginLayer = function() {
var div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
document.appendChild(div)
return div
}
var createSingleLoginLayer = getsingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function() {
var loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}
观察者模式
定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解。
场景:promise中 then函数中的回调就订阅了resolve函数和reject函数;vue中生命周期钩子
我们解读一下代码:
- 首先,主题对象 需要有
状态(值)的get、set方法
;观察者的绑定解绑方法
;通知观察者的方法
- get 方法直接返回状态即可,set方法设置状态后,调用通知方法
- attach方法绑定观察者,其实就是加入数组;unattach找到对应的观察者移除即可
- notifyAllObservers方法就是遍历数组调用update方法
- 对于 观察者来说 具有自己的属性,要绑定哪个主题,以及update方法
- new 观察者时,设置自己的属性,然后根据传入的主题进行好绑定
- 接到通知后,调用update方法,再使用提供的get方法获取状态改变的值
- 对于解绑来说,调用主体不同写法不同
// 主题 保存状态,状态变化之后触发所有观察者对象
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
attach(observer) {
this.observers.push(observer)
}
unattach(observer) {
this.observers.splice(this.observers.indexOf(observer),1)
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update()
})
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state: ${this.subject.getState()}`)
}
}
// 测试
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
s.setState(12)
// 两种方法都可以解除绑定
// s.unattach(o1) //以主题为主体
o1.subject.unattach(this) // 以观察者为主体
s.setState(13)
发布者-订阅者模式
其实仔细看看,EventEmitter
就是一个典型的发布订阅模式,实现了事件调度中心。发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。我们刚刚实现的EventEmitter
的一个实例ee
就是一个事件调度中心,发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的emit
方法发布事件,而订阅者则通过on
进行订阅。
下面解读一下代码:
- 事件调度中心需要实现:维护订阅者,订阅者注册方法
on
,事件发布方法emit
,移除某个订阅者或全部订阅者的方法off
on
方法:如果没有该类事件,我们先创建一个数组(维护cb),然后再将回调加入emit
方法:发布事件,如果有人订阅该事件,则通知所有人执行回调。如果没有发布也就没人响应off
方法:在移除订阅者时,如果该事件有人订阅,则找到对应的人进行移除,移除后如果订阅者为空,就删除这个事件维护的cb数组offAll
方法:如果该事件有订阅者,直接移除事件维护的cb数组即可
class EventEmitter {
constructor() {
// 维护事件及监听者
this.listeners = {}
}
/**
* 注册事件监听者
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
on(type, cb) {
if (!this.listeners[type]) {
this.listeners[type] = []
}
this.listeners[type].push(cb)
}
/**
* 发布事件
* @param {String} type 事件类型
* @param {...any} args 参数列表,把emit传递的参数赋给回调函数
*/
emit(type, ...args) {
if (this.listeners[type]) {
this.listeners[type].forEach(cb => {
cb(...args)
})
}
}
/**
* 移除某个事件的一个监听者
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
off(type, cb) {
if (this.listeners[type]) {
const targetIndex = this.listeners[type].findIndex(item => item === cb)
if (targetIndex !== -1) {
this.listeners[type].splice(targetIndex, 1)
}
if (this.listeners[type].length === 0) {
delete this.listeners[type]
}
}
}
/**
* 移除某个事件的所有监听者
* @param {String} type 事件类型
*/
offAll(type) {
if (this.listeners[type]) {
delete this.listeners[type]
}
}
}
// 创建事件管理器实例
const ee = new EventEmitter()
// 注册一个chifan事件监听者
ee.on('chifan', function() { console.log('吃饭了,我们走!') })
// 发布事件chifan
ee.emit('chifan')
// 也可以emit传递参数
ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) })
ee.emit('chifan', '三食堂', '铁板饭') // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者
// 测试移除事件监听
const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') }
ee.on('testoff', toBeRemovedListener)
ee.emit('testoff')
ee.off('testoff', toBeRemovedListener)
ee.emit('testoff') // 此时事件监听已经被移除,不会再有console.log打印出来了
// 测试移除chifan的所有事件监听
ee.offAll('chifan')
console.log(ee) // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了
工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。例如我们axios中使用create创建就类似;曾经我们熟悉的JQuery的$()就是一个工厂函数,它根据传入参数的不同创建元素或者去寻找上下文中的元素
场景:将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式。
缺点:添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度。考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()