设计模式运用广泛,前端中也存在很多设计模式,将目前遇到过并且识别出的设计模型进行一下记录。
1.发布订阅模式
on:一般为订阅者命名,将异步函数加入数组,等待被执行;
emit:一般为发布者命名,从数组中取出异步函数,一次进行执行
发布订阅之间没有明显的关系,他们主要是靠中介(数组)。与下面的观察者模式有所区别。
【注】
目前了解的发布订阅模式的使用有:
- vue2中的数据响应就是基于发布订阅模式实现的;
- mqtt通信
- websocket
class EventBus {
constructor() {
this.tasks = {};
}
// 1.订阅模式 - 根据type,进行存储数组
on(type, cb) {
if (!this.tasks[type]) {
this.tasks[type] = [];
}
this.tasks[type].push(cb);
}
// 2.发布模式 - 根据type去除对应的数组,依次执行异步函数
emit(type, ...args) {
let arr = this.tasks[type];
if (arr?.length) {
arr.forEach((i) => i(args)); // 注意this指向);
}
}
// 3.删除事件
off(type, fn) {
this.tasks[type] = this.tasks[type].filter((item) => item !== fn);
}
// 4.只执行一次
once(type, fn) {
let f = (...args) => {
fn(...args);
this.off(type, f);
};
this.on(type, f); // 将f函数加入,则emit多次的时候,该函数执行完后立即被删除,达到了只运行一次的目的
}
}
let bus = new EventBus();
let fn1 = (...args) => {
console.log("订阅了1", ...args);
};
let fn2 = (...args) => {
console.log("订阅了2", ...args);
};
bus.on("change", fn1);
bus.once("change", fn2);
// bus.off("change", fn2); // 取消了fn2的订阅,则后续无法执行对应的异步函数
bus.emit("change", 1, 2, 3);
bus.emit("change", 1, 2, 3);
bus.emit("change", 1, 2, 3);
遗留问题:那先订阅后发布如何实现?
待看
2.观察者模式
是观察者和被观察者的关系,一旦被观察者的属性发生了变化,则会通知观察者,做出相应的动作。
【注】
类似于vue2中数据响应式更新的数据劫持,在get阶段收集依赖,然后在set阶段触发依赖,实现对应的依赖异步函数依次执行,实现数据变化触发视图更新。
// 被观察者
class Subject {
constructor(name) {
this.name = name;
this.state = "非常开心";
this.observers = [];
}
// 1.收集观察者(有没有觉得很像订阅)
attach(o) {
this.observers.push(o);
}
setState(state) {
this.state = state;
// 2.一更新状态,则会主动通知观察者
this.observers.forEach((item) => {
item.update(this);
});
}
}
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
update(baby) {
console.log(`当前:${this.name}被通知了,小宝宝状态变化:` + baby.state);
}
}
// 简单的例子:观察者,需要观察小宝宝的心理变化
let baby = new Subject("baby");
let father = new Observer("father");
let mother = new Observer("mother");
baby.attach(father); //把观察者加入到被观察中
baby.attach(mother);
baby.setState("被欺负了");
// 当前:father,知道了小宝宝状态变化:被欺负了
// 当前:mother,知道了小宝宝状态变化:被欺负了
3.单例模式
单例模式两个条件
(1)确保只有一个实例
(2)可以全局访问
【注】 适用场景:“单例模式的特点,意图解决:维护一个全局实例对象。”
- 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
- 弹窗(登录框,信息提示框)
- 购物车 (一个用户只有一个购物车)
- 全局态管理 store (Vuex )
class MyObject {
constructor(name) {
this.name = name;
return this.name;
}
static getInstance(name) {
if (!this.instance) {
this.instance = new MyObject(name);
}
return this.instance;
}
}
let obj = MyObject.getInstance("mm");
console.log(obj.__proto__ === MyObject.prototype);
4.工厂模式
- 工厂模式是用来创建对象的一种最常用的设计模式。
- 在实际的前端业务中,最常用的简单工厂模式。
- 什么时候会用工厂模式?将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式。
//1.简单工厂模式/静态工厂模式
// 前端应用:根据用户的权限来渲染不同的页面
class userFactory {
constructor(name,pages) {
this.name = name;
this.viewPages = pages
}
static getInstance(roleEn) {
switch (roleEn) {
case "admin":
return new userFactory('超级管理员',['主页','用户信息页'])
case "normal":
return new userFactory('超级管理员', ['用户信息页'])
default: return null
}
}
}
let admin = userFactory.getInstance('admin')
let normal = userFactory.getInstance('normal')
//2.vue中的实现根据用户类型返回不同的路由权限
//2.1routerFactory.js
import SuperAdmin from '../components/SuperAdmin.vue'
import NormalAdmin from '../components/Admin.vue'
import User from '../components/User.vue'
import NotFound404 from '../components/404.vue'
let AllRoute = [
//超级管理员页面
{
path: '/super-admin',
name: 'SuperAdmin',
component: SuperAdmin
},
//普通管理员页面
{
path: '/normal-admin',
name: 'NormalAdmin',
component: NormalAdmin
},
//普通用户页面
{
path: '/user',
name: 'User',
component: User
},
//404页面
{
path: '*',
name: 'NotFound404',
component: NotFound404
}
]
let routerFactory = (role) => {
switch (role) {
case 'superAdmin':
return {
name: 'SuperAdmin',
route: AllRoute
};
case 'normalAdmin':
return {
name: 'NormalAdmin',
route: AllRoute.splice(1)
}
case 'user':
return {
name: 'User',
route: AllRoute.splice(2)
}
default:
throw new Error('参数错误! 可选参数: superAdmin, normalAdmin, user')
}
}
export { routerFactory }
//2.2Login.vue
import {routerFactory} from '../router/routerFactory.js'
export default {
//...
methods: {
userLogin() {
//请求登陆接口, 获取用户权限, 根据权限调用this.getRoute方法
},
getRoute(role) {
//根据权限调用routerFactory方法
let routerObj = routerFactory(role);
//给vue-router添加该权限所拥有的路由页面
this.$router.addRoutes(routerObj.route);
//跳转到相应页面,注,因为使用this.$router.addRoutes方法添加的路由刷新后不能保存,所以会导致路由无法访问。
//通常的做法是本地加密保存用户信息,在刷新后获取本地权限并解密,根据权限重新添加路由。
this.$router.push({name: routerObj.name})
}
}