什么是设计模式?
在软件开发中,设计模式是解决特定问题的经验总结和可复用的解决方案。设计模式可以提高代码的复用性、可维护性和可读性,是提高开发效率的重要手段。
单例模式
1.概念
单例模式 (Singleton Pattern)
,保证一个类只有一个实例,并提供一个访问它的全局访问点。也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象。
2.代码实现
class Singleton {
static instance;
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
3.应用场景
- 浏览器中的 window 和 document 全局变量,这两个对象都是单例,任何时候访问他们都是一样的对象,window 表示包含 DOM 文档的窗口,document 是窗口中载入的 DOM 文档,分别提供了各自相关的方法。
- element-ui 中以服务方式调用的Loading 是单例的。
- 项目中的全局状态管理模式 Vuex 维护的全局状态,vue-router`维护的路由实,在单页应用的单页面中都属于单例的应用。
4.优缺点
优点
- 内存中只有一个实例,减少内存开销,尤其是频繁创建和销毁实例时(如管理学院首页页面缓存)。
- 避免资源的多重占用(如写文件操作)。
缺点
- 扩展不友好:没有接口,不能继承。
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心实例化方式。
二、发布订阅模式
1.概念
JavaScript 发布订阅模式(Publish/Subscribe Pattern)
是一种常用的设计模式。在发布订阅模式中,事件的发生者(发布者)不需要直接调用事件的处理者(订阅者),而是通过一个「发布-订阅中心」
来管理事件的发生和处理。具体来说,发布者将事件发布到「发布-订阅中心」
中,订阅者可以向「发布-订阅中心」注册事件处理函数,当事件发生时,「发布-订阅中心」会将事件通知给所有注册了该事件处理函数的订阅者,订阅者就可以处理该事件。
发布订阅模式的核心思想是解耦事件的发生和事件的处理,使得事件发生者和事件处理者之间不直接依赖,从而提高程序的灵活性和可维护性。
2.代码实现
/**
* @description: 发布订阅
* @return {*}
*/
// events: {
// [key]: [{fn: Function, isOnce: Boolean},],
// [key2]: [{fn: Function, isOnce: Boolean}, ],
// }
class EventBus {
events = {}
constructor() {
this.events = {}
}
// 注册事件
on(key, fn, isOnce = false) {
if (!key) return
const currentEvents = this.events[key]
if (!currentEvents) {
this.events[key] = []
}
this.events[key].push({ fn, isOnce })
}
// once
once(key, fn) {
this.on(key, fn, true)
}
// 触发事件
emit(key, ...args) {
if (!key) return
const currentEvents = this.events[key]
if (!currentEvents || !currentEvents.length) {
return
}
this.events[key] = currentEvents.filter((item) => {
item.fn(...args)
return !item.isOnce
})
}
// 取消订阅
off(key, fn = undefined) {
if (!this.events[key]) return
if (!fn) {
this.events[key] = []
} else {
this.events[key] = this.events[key].filter((_) => _.fn !== fn)
}
}
}
const e = new EventBus()
function fn1(a, b) { console.log('fn1', a, b) }
function fn2(a, b) { console.log('fn2', a, b) }
function fn3(a, b) { console.log('fn3', a, b) }
e.on('key1', fn1)
e.on('key1', fn2)
e.once('key1', fn3)
e.emit('key1', 10, 20) // 触发 fn1 fn2 fn3
e.off('key1', fn1)
e.emit('key1', 100, 200) // 触发 fn2
3.应用场景
- Vue事件总线(EventBus)、 o n 、 on、 on、emit、$off, 跨组件传值
- Vue3事件总线 使用 mitt
- DOM事件监听:给DOM元素绑定一个事件(如click事件),当对应的交互触发时,我们绑定的事件就会被触发。
4.优缺点
优点
- 发布-订阅模式的有点非常明显,一为时间上的解耦,二为对象之间的解耦。
- 应用广泛,既可以用在异步编程中,也可以帮助我们更松耦合的代码编写。
缺点
- 创建订阅者本身要消耗一定的时间和内存,而且当订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。
- 发布-订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象与对象之间必要的联系也将被深埋在背后,会导致程序难以跟踪维护和理解。
二、观察者模式
1.概念
观察者模式(Observer Pattern)
是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
2.代码实现
let observerId = 1;
let observedId = 10;
//观察者类
class Observer {
constructor() {
this.id = observerId++;
}
//观测到变化后的处理
update(ob) {
console.log("观察者" + this.id + `-检测到被观察者${ob.id}变化`);
}
}
//被观察者列
class Observed {
constructor() {
this.observers = [];
this.id = observedId++;
}
//添加观察者
addObserver(observer) {
this.observers.push(observer);
}
//删除观察者
removeObserver(observer) {
this.observers = this.observers.filter(o => {
return o.id != observer.id;
});
}
//通知所有的观察者
notify() {
this.observers.forEach(observer => {
observer.update(this);
});
}
}
let mObserved = new Observed();
let mObserver1 = new Observer();
let mObserver2 = new Observer();
mObserved.addObserver(mObserver1);
mObserved.addObserver(mObserver2);
mObserved.notify();
3.应用场景
微信公众号,如果一个用户订阅了某个公众号,那么便会收到公众号发来的消息,那么,公众号就是『被观察者』,而用户就是『观察者』
观察者模式 VS 发布订阅模式
对象关系
- 观察者模式中,被观察者和观察者之间的关系是
一对多
的关系,即一个被观察者可以有多个观察者,但是每个观察者只关注一个被观察者。被观察者维护一个观察者列表,当被观察者发生变化时,通知所有观察者进行相应的处理。 - 发布订阅模式中,发布者和订阅者之间的关系是
多对多
的关系,即一个发布者可以有多个订阅者,每个订阅者可以关注多个发布者。发布者和订阅者之间通过「发布-订阅中心」进行通信,当发布者发生变化时,通知所有订阅者进行相应的处理。
解耦
- 在观察者模式中,
被观察者和观察者之间的通信是直接的
,即被观察者会直接调用观察者的方法进行通信。这种直接的通信方式可能会导致被观察者与观察者之间的耦合度较高。 - 在发布订阅模式中,
发布者和订阅者之间的通信是通过「发布-订阅中心」进行的,即发布者不直接与订阅者通信
,而是通过「发布-订阅中心」进行通信。这种间接的通信方式可以降低发布者与订阅者之间的耦合度。
三、策略模式
1.概念
策略模式(Strategy Pattern)
指的是定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。
⽬的就是将策略的实现和使用分离。
Context :封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;
Strategy :策略,含有具体的算法,其方法的外观相同,因此可以互相代替;
StrategyMap :所有策略的合集,供封装上下文调用;
2.应用(表单校验)
表单验证是一个常见的应用场景,而策略模式可以很好地应用于表单验证的实现。通过策略模式,可以将不同的验证规则封装成策略对象,根据具体的情况选择相应的验证策略进行验证。这样可以实现更加灵活和可扩展的表单验证功能。
// 定义表单验证策略对象
const strategies = {
isNotEmpty(value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isEmail(value, errorMsg) {
const emailReg = /^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$/;
if (!emailReg.test(value)) {
return errorMsg;
}
},
minLength(value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
};
// 表单验证类
class Validator {
constructor() {
this.rules = [];
}
addRule(value, rule, errorMsg) {
this.rules.push(() => strategies[rule](value, errorMsg));
}
validate() {
for (let rule of this.rules) {
const errorMsg = rule();
if (errorMsg) {
return errorMsg;
}
}
}
}
// 使用策略模式进行表单验证
const validator = new Validator();
validator.addRule('example@qq.com', 'isNotEmpty', '邮箱不能为空');
validator.addRule('example@qq.com', 'isEmail', '邮箱地址无效');
const error = validator.validate();
if (error) {
console.log(error);
} else {
console.log('表单验证通过');
}
4.优缺点
优点
- 算法切换自由:可以在运行时根据需要切换算法。
- 避免多重条件判断:消除了复杂的条件语句。
- 扩展性好:新增算法只需新增一个策略类,无需修改现有代码。
缺点
- 策略类数量增多:每增加一个算法,就需要增加一个策略类。
- 所有策略类都需要暴露:策略类需要对外公开,以便可以被选择和使用。
四代理模式
1.概念
为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
代码举例
1、图片懒加载
//图片加载
let imageEle = (function(){
let node = document.createElement('img');
document.body.appendChild(node);
return {
setSrc:function(src){
node.src = src;
}
}
})();
//代理对象
let proxyImage = (function(){
let img = new Image();
img.onload = function(){
imageEle.setSrc(this.src);
};
return {
setSrc:function(src){
img.src = src;
imageEle.setSrc('loading.gif');
}
}
})();
proxyImage.setSrc('example.png');
2.缓存代理
缓存代理可以作为一些开销大的运算结果提供暂时的存储,下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
//计算乘积
let mult = function(){
let result = 1;
for(let i = 0,len = arguments.length;i < len;i++){
result*= arguments[i];
}
return result;
}
//缓存代理
let proxy = (function(){
let cache = {};
reutrn function(){
let args = Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args] = mult.apply(this,arguments);
}
})();
3.优缺点
优点
- 职责分离:代理模式将访问控制与业务逻辑分离。
- 扩展性:可以灵活地添加额外的功能或控制。
- 智能化:可以智能地处理访问请求,如延迟加载、缓存等。
缺点 - 性能开销:增加了代理层可能会影响请求的处理速度。
- 实现复杂性:某些类型的代理模式实现起来可能较为复杂。
注:更多设计模式待后续补充…
参考链接
- https://juejin.cn/post/6844904032826294286?searchId=202407012147303848AD829D3DBC1E62B7
- https://juejin.cn/post/7372514352425926668
- https://juejin.cn/post/6844904138707337229?searchId=20240701174747B0D1A8A77127F94493AD#heading-19
- https://www.runoob.com/design-pattern/design-pattern-tutorial.html