说到设计模式,其实它并不是我们在开发过程中,必须要去套用的。它是我们在解决问题的时候针对特定问题给出的简洁而优化的处理方案, 是软件开发常用解决问题的经验总结,用于特定的情境下复用,它没有语言的限制,它存在的目的就是为了提高代码的可维护性,可扩展性和重用性。 我们一起来看下前端开发中常用的7种设计模式🚀。
单例模式
主要特征
- 保证一个类仅有一个实例 ,并且可以全局访问
- 主要解决一个全局使用的类频繁地创建和销毁,占用内存
基本语法
闭包实现
js
const Singleton = (function () {
let instance;
// 创建单例对象的代码
function CreateInstance(name) {
this.name = name
}
return function(name,age){
if(!instance){
//创建实例
instance = new CreateInstance(name)
}
return instance
}
})();
// test
const a = new Singleton('a') // 通过 getInstance 来获取实例
const b = new Singleton('b')
console.log(a === b) // true
class 实现单例
js
class Singleton{
constructor(name){
if(!Singleton.instance){
this.name = name
Singleton.instance = this
}
return Singleton.instance
}
}
实践
实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销,我们可以在需要弹框的时候再进行创建, 并且单例模式也可以保证创建之后,保存到内存中,防止全局使用的类频繁的创建和销毁,占用内存。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button id='open'>打开弹框</button>
<button id='close'>关闭弹框</button>
</body>
<script>
const Modal = (function () {
let modal = null
return function () {
if (!modal) {
modal = document.createElement('div')
modal.innerHTML = '登录对话框'
modal.className = '-modal'
modal.style.display = 'none'
document.body.appendChild(modal)
}
return modal
}
})()
document.querySelector('#open').addEventListener('click', function () {
const modal = new Modal()
modal.style.display = 'block'
})
document.querySelector('#close').addEventListener('click', function () {
const modal = new Modal()
modal.style.display = 'none'
})
</script>
</html>
应用场景
- 网页中的全局状态管理器(redux,vuex等)
- 全局弹窗,提示等组建
策略模式
主要特征
定义: 根据不同参数可以命中不同的策略
该模式主要解决 在有多种算法相似的情况下,使用 if...else
所带来的复杂和难以维护。通过策略模式来简化if...else
语句。它的优点 是算法可以自由切换,同时可以避免多重if...else
判断,且具有良好的扩展性。
它可以帮助我们更好地组织和处理 复杂的业务逻辑。通过将不同的算法或逻辑封装成独立的策略对象,使得这些策略对象可以在运行时根据需要进行切换或替换,从而实现灵活性和可扩展性。
基本语法
策略模式的写法有很多,但是思想都是一样的,根据不同参数可以命中不同的策略。
js
const strategy = {
'A': function (name) {
return name
},
'B': function (name) {
return name
},
'C': function (name) {
return name
},
}
const getStrategy = (level, name) => {
return strategy[level](name)
}
getStrategy('B', '惊蝉')
实践
js
const loginForm = function (account, pwd) {
if (account === null || account === '') {
alert('手机号不能为空')
return false;
}
if (pwd === null || pwd === '') {
alert('密码不能为空')
return false;
}
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) {
alert('手机号格式错误')
return false;
}
if (pwd.length < 6) {
alert('密码不能小于六位')
return false;
}
// ajax 发送请求
}
在表单验证中,对账号和密码验证登陆,每新增一种、或者修改原有校验规则,我们都必须去改新加 if
判断代码 。另外逻辑的复用性也很差, 如果有其它表单也是用同样的规则,这段代码并不能复用,只能复制。
使用策略模式改进代码
js
var strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '' || value === null) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // 手机号码格式
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
}
};
const loginForm = function (account, pwd) {
const isNonEmpty = strategies.isNonEmpty(account, '手机号不能为空')
const isMobile = strategies.isMobile(pwd, '手机号格式不正确')
const minLength = strategies.minLength(pwd, 6, '密码不能小少6位')
const errorMsg = isNonEmpty || isMobile || minLength
if(errorMsg) {
alert(errorMsg)
return false
}
// ajax 发送请求
}
对比两种实现,我们可以看到:分离了校验逻辑的代码如果需要扩展校验类型,在策略组中新增定义即可使用; 如果需要修改某个校验的实现,直接修改相应策略即可全局生效。对于开发和维护都有明显的效率提升。
优点
- 灵活性高 :减少了
if
判断,策略模式允许动态切换算法或策略,使得程序可以根据不同的需求使用不同的策略,提高了代码的灵活性。 - 可扩展性好:添加新的策略只需要实现一个新的策略函数即可,不需要修改已有的代码,可以很方便地对代码进行扩展。
应用场景
- 多个表单校验
- 运行时根据不同情况选择不用的算法或行为
- 全局配置文件等
观察者模式
主要特征
观察者模式包含观察目标和观察者两类对象。
主要特点:
- 一个目标可以有任意数目的与之相依赖的观察者
- 一旦观察目标的状态发生改变,所有的观察者都将得到通知
即当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
基本语法
js
class Sub {
constructor() {
this.observers = []
}
addObserver(observer) {
if (!observer) return
this.observers.push(observer)
}
removeObserver(observer) {
if (!observer) return
this.observers = this.observers.filter(item => item !== observer)
}
notifyOvservers(data) {
this.observers.forEach(observer => {
observer.update(data)
})
}
}
class Ovserver {
constructor(name) {
this.name = name
}
upadate(data) {
console.log(this.name + ' received data: ' + data);
}
}
const ob1 = new Ovserver('jing')
const ob2 = new Ovserver('chan')
const sub = new Sub()
sub.addObserver(ob1)
sub.addObserver(ob2)
sub.notifyOvservers('hello world')
实践
Vue的双向绑定
Object.defineProperty
使用Object.defineProperty(obj, props, descriptor)
实现观察者模式, 其也是 vue 双向绑定 的核心, 示例如下(当改变 obj 中的 value 的时候, 自动调用相应相关函数):
js
const obj = {
data: { list: [] }
}
Object.defineProperty(obj, 'list', {
get() {
return this.data['list']
},
set(val) {
console.log('值被更改了')
this.data['list'] = val
}
})
Proxy
Proxy/Reflect 是 ES6 引入的新特性, 也可以使用其完成观察者模式, 示例如下(效果同上):
js
const obj = {
value: 0
}
const proxy = new Proxy(obj, {
set(target, key, value, receiver) { // {value: 0} "value" 1 Proxy {value: 0}
console.log('值被更改了')
Reflect.set(target, key, value, receiver)
}
})
proxy.value = 1
vue 在 3.0 版本上使用 Proxy 重构的原因
首先罗列 Object.defineProperty() 的缺点:
- Object.defineProperty() 不会监测到数组引用不变的操作(比如 push/pop 等);
- Object.defineProperty() 只能监测到对象的属性的改变, 即如果有深度嵌套的对象则需要再次给之绑定 Object.defineProperty();
关于 Proxy 的优点
- 可以劫持数组的改变;
- defineProperty 是对属性的劫持, Proxy 是对对象的劫持;
应用场景
- 用户界面中的事件监听
- 数据变化时的通知和更新
发布订阅模式
主要特征
在发布-订阅模式中,有一个中介者(通常称为"事件总线"或"消息队列") 来管理订阅者和发布者之间的关系。发布者和订阅者不直接耦合,它们通过中介者进行通信。发布者将消息发送到中介者,然后中介者将消息传递给所有订阅者。 订阅者通过向中介者注册感兴趣的事件或主题,中介者在接收到消息后负责将消息分发给所有订阅者。
发布者和订阅者不用互相知道,通过第三方实现调度,属于经过解耦合的观察者模式。
基本语法
js
class PubSub {
constructor() {
this.message = {}
}
publish(type, data) {
this.message[type].forEach(item => item(data));
}
subscribe(type, cb) {
if (!this.message[type]) {
this.message[type] = [cb]
} else {
this.message[type].push(cb)
}
}
unsubscribe(type, cb) {
if (!this.message[type]) return
if (!cb) {
this.message[type] && (this.message[type].length = 0)
} else {
this.message[type] = this.message[type].filters(item => item !== cb)
}
}
}
export default new PubSub()
实践
在前端开发中,许多库和框架使用了发布-订阅模式或类似的机制,以便实现组件通信、事件处理、状态管理等功能。比如:PubSubJs
、Vue 的 EventBus
,jQuery
等。
React 中,没有 Vue 那样直接提供了发布-订阅模式,React 更倾向于单向数据流 props 和 回调函数 进行组件通信。以下结合一下 React 的 Context API
实现一个发布-订阅模式。
功能实现代码:
js
// EventBusContext.js
import { createContext, useContext } from 'react';
const EventBusContext = createContext();
export const useEventBus = () => {
return useContext(EventBusContext);
};
export const EventBusProvider = ({ children }) => {
const listeners = {};
const subscribe = (eventName, callback) => {
if (!listeners[eventName]) {
listeners[eventName] = [];
}
listeners[eventName].push(callback);
return () => {
listeners[eventName] = listeners[eventName].filter(cb => cb !== callback);
};
};
const publish = (eventName, data) => {
if (listeners[eventName]) {
listeners[eventName].forEach(callback => {
callback(data);
});
}
};
const value = {
subscribe,
publish
};
return (
<EventBusContext.Provider value={value}>
{children}
</EventBusContext.Provider>
);
};
使用代码:
js
// ComponentA.js
import React from 'react';
import { useEventBus } from './EventBusContext';
const ComponentA = () => {
const eventBus = useEventBus();
const sendMessage = () => {
eventBus.publish('message', 'Hello from ComponentA!');
};
return (
<div>
<button onClick={sendMessage}>Send Message</button>
</div>
);
};
export default ComponentA;
javascript
// ComponentB.js
import React, { useEffect } from 'react';
import { useEventBus } from './EventBusContext';
const ComponentB = () => {
const eventBus = useEventBus();
useEffect(() => {
const unsubscribe = eventBus.subscribe('message', message => {
console.log('Received message in ComponentB:', message);
});
return () => {
unsubscribe();
};
}, [eventBus]);
return <div>ComponentB</div>;
};
export default ComponentB;
应用场景
- 页面上的事件监听和处理
- 消息传递和通信
工厂模式
主要特征
由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
基本语法
js
function User(name, age, career, work) {
this.name = name
this.age = age
this.career = career
this.work = work
}
function Factory(name, age, career) {
let work;
switch(career) {
case 'coder':
work = ['写代码', '改bug', '骂产品']
break
case 'product manager':
work = ['订会议室', '画原型图', '催更']
break
// ...
}
return new User(name, age, career, work)
}
const factory = new Factory('惊蝉', 18, 'coder')
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。但是在函数内包含了所有对象的创建逻辑和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。当我们的对象不是上面的3个而是10个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
应用场景
- 数据源选择 ---> 后台管理系统中,返回相应权限人员的信息
- 集中管理对象创建,降低代码复杂度
适配器模式
主要特征
将一个类的接口转换成客户希望的另一个接口,让那些接口不兼容的类可以一起工作。
基本语法
js
//按照官网代码复制
class TencentMap {
show() {
console.log('开始渲染腾讯地图');
}
}
//按照官网代码复制
class BaiduMap {
display() {
console.log('开始渲染百度地图');
}
}
class BaiduMapAdapter extends BaiduMap {
constructor() {
super();
}
render() {
this.display();
}
}
class TencentMapAdapter extends TencentMap {
constructor() {
super();
}
render() {
this.show();
}
}
// 外部调用者
function renderMap(map) {
map.render(); // 统一接口调用
}
renderMap(new TencentMapAdapter());
renderMap(new BaiduMapAdapter());
腾讯地图和百度地图的渲染地图的方法不同,我们不可能去改变它们的底层逻辑,通过适配器模式将不同名字的方法统一名字调用。
适配器不会去改变实现层,那不属于它的职责范围,它干涉了抽象的过程。外部接口的适配能够让同一个方法适用于多种系统。
应用场景
- axios 请求中,在
浏览器环境
和Node环境
,自动适配环境,在浏览器中采用XMLHttpRequest
请求数据,但是在 Node 环境中采用时http
模块请求数据。 - 数据格式、类函数方法等适配
装饰器模式
主要特征
装饰器模式能够很好的对已有功能进行拓展,这样不会更改原有的代码,对其他的业务产生影响,这方便我们在较少的改动下对软件功能进行拓展。
装饰器模式的主要用途是为一个原始对象添加新的功能或者修改已有的功能,同时保持原始对象的接口不变,并且可以在运行时动态地添加或删除功能。
基本语法
js
// 定义装饰器,前置调用
Function.prototype.before = function (beforeFn) {
var _this = this;
return function () {
beforeFn.apply(this, arguments);
return _this.apply(this, arguments);
};
};
// 定义装饰器,后置调用
Function.prototype.after = function (afterFn) {
var _this = this;
return function () {
var ret = _this.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
};
};
// 原始函数
function test() {
console.log("11111")
}
var test1 = test.before(() => {
console.log("00000")
}).after(()=>{
console.log("22222")
})
test1()
在函数调用前后使用新增before
、after
函数进行调用,axios 的 intercepters 拦截器使用的就是前置装饰器。
应用场景
- 在不修改原始对象的情况下,为其添加额外的功能
- 在运行时动态地为对象添加新行为