设计模式是某种思想,这种思想可以更规范更合理去管理代码,方便维护、升级、扩展、开发。
单例模式
Singleton单例模式 && Command命令模式
举例:
// 公用版块 utils
let utils = (function () {
function debounce(func, wait) {}
//...
return {
debounce: debounce
};
})();
// A版块
let AModule = (function () {
utils.debounce();
function fn() {}
function query() {}
return {
query: query
};
})();
// B版块
let BModule = (function () {
function getData() {}
return {
getData() {}
};
})();
为了保证每个板块私有化,将每个板块单独写到一个闭包里面。
此时,其中的一个板块utils就叫做命名空间,它代表的就是最后return回来的那个对象,即那块空间的名字。它包含了暴露到外面的供别的模块调用的接口。这个板块就算是一个单例,这种组织代码的思想就叫做单例模式
其实直接声明一个对象,并且将一些东西放到那个对象中,那个对象也叫做命名空间,这也属于单例模式,只不过实现这种思想的方式仅仅是通过了一个对象,比较简单
而结合闭包的模块化,就可以算是高级的单例模式
作用:
- 最早期的模块化编程思想「同样的还有AMD/CMD/CommonJS/ES6Module」
- 避免全局变量的污染
- 实现模块之间的相互调用「提供了模块导出的方案」
总结:
- 什么是单例模式?
- 其实声明一个对象,并将一些方法和属性放到对象中就属于单例模式,那个对象也叫做命名空间。只不过实现这种思想的方式仅仅是通过了一个对象,比较简单
- js中还有一种比较优雅的单例模式方法,就是立即执行函数返回一个对象,这个对象向外暴露接口供别的模块调用,这种使用闭包的方式时早起的模块化思想,也可以避免全局变量的污染。
命令模式
基于单例模式。
在实际的业务开发中,我们还可以基于命令模式管控方法的执行顺序,从而有效的实现出对应的功能。
// B版块{实现当前模块下需要完成的所有的功能}
let BModule = (function () {
utils.debounce();
AModule.query();
// 获取数据
function getData() {}
// 绑定数据
function binding() {}
// 处理事件绑定
function handle() {}
// 处理其它事情的
function fn() {}
return {
init() {
// 模块的入口「相当于模块的大脑,控制模块中方法的执行顺序」
getData();
binding();
handle();
fn();
}
};
})();
BModule.init();
BModule.init()实现了控制模块中方法的执行顺序的功能,命令各种方法(功能模块)按照一定的顺序执行,这种模式就是命令模式。
早期基于原生js或者jquery开发的时候,都是基于单例模式,命令模式进行代码组织和开发的。
发布订阅模式
Publish & Subscribe 发布订阅模式
过程基本如下:
- 创建事件池 / 发布计划
- 向事件池中加入方法 / 向计划表中订阅任务
- fire(触发事件) / 通知计划表中的任务执行
应用场景:凡是某个阶段到达的时候,需要执行很多方法「更多时候,到底执行多少个方法不确定,需要编写业务边处理的」,我们都可以基于发布订阅设计模式来管理代码;
基本的发布订阅模式代码:
(function () {
// 自己创造的事件池
let pond = [];
// 向事件池中注入方法
function subscribe(func) {
// 去重处理
if (!pond.includes(func)) {
pond.push(func);
}
// 每一次执行,返回的方法是用来移除当前新增的这一项的
return function unsubscribe() {
pond = pond.filter(item => item !== func);
};
}
// 通知事件池中的每个方法执行
subscribe.fire = function fire(...params) {
pond.forEach(item => {
if (typeof item === "function") {
item(...params);
}
});
};
window.subscribe = subscribe;
})();
使用:
例如一个需求:希望从服务获取数据,并且获取数据后要对数据做很多处理
// 需求:从服务获取数据,获取数据后要干很多事情
// A
const fn1 = data => {
console.log('fn1',data)
};
subscribe(fn1);
// B
const fn2 = data => {
console.log('fn2',data)
};
subscribe(fn2);
// C
const fn3 = data => {
console.log('fn3',data)
};
const unsubscribe = subscribe(fn3);
unsubscribe()
function query(){
return new Promise(resolve=>{
setTimeout(()=>{
resolve('query data')
},0)
})
}
query().then(data => {
subscribe.fire(data)//触发事件
});
如果使用下面这种方式
query().then(data => {
fn1(data);
fn2(data);
fn3(data);
fn4(data);
});
如果以后有新增一个方法fn5,那么还需要把方法加入到代码当中去,如果用了设计模式后,代码会变得更好一点
多个事件池、不同的事件类型与面向对象
一个项目中,我们可能会出现多个事情都需要基于发布订阅来管理,一个事件池不够,并且每次使发布订阅模式都需要重新
我们想实现
- 管理多个事件池
- 每个事件池支持不同的自定义事件类型
- 通过面向对象中类&实例的方式来进行抽象化,好处:
- 让每个实例都有一个自己的私有事件池
subscribe/unsubscribe/fire方法是公用的,可以抽象在公有属性中 实现如下:
面向对象
首先改为面向对象的模式:
class Sub {
// 实例私有的属性:私有的事件池
pond = [];
// 原型上设置方法:向事件池中订阅任务
subscribe(func) {
let self = this,
pond = self.pond;
if (!pond.includes(func)) pond.push(func);
return function unsubscribe() {//移除,也可以用filter
let i = 0,
len = pond.length,
item = null;
for (; i < len; i++) {
item = pond[i];
if (item === func) {
pond.splice(i, 1);
break;
}
}
};
}
// 通知当前实例所属事件池中的任务执行
fire(...params) {
let self = this,
pond = self.pond;
pond.forEach(item => {
if (typeof item === "function") {
item(...params);
}
});
}
}
使用:
let sub1 = new Sub;
sub1.subscribe(function () {
console.log(1, arguments);
});
sub1.subscribe(function () {
console.log(2, arguments);
});
setTimeout(() => {
sub1.fire(100, 200);
}, 1000);
let sub2 = new Sub;
sub2.subscribe(function () {
console.log(3, arguments);
});
sub2.subscribe(function () {
console.log(4, arguments);
});
setTimeout(() => {
sub2.fire(300, 400);
}, 2000);
多个事件池
我们用以下数据结构来设置事件池
pond = {
A:[fn1,fn2],
B:[fn1,fn2]
}
约定成俗地,几个操作的名字分别是:监听事件on, 移除事件 off, 触发事件 emit。也可以用add remove fire。这里我们用on,off,emit
整体结构如下:
let sub = (function () {
let pond = {};
// 向事件池中追加指定自定义事件类型的方法
const on = function on(type, func) {};
// 从事件池中移除指定自定义事件类型的方法
const off = function off(type, func) {};
// 通知事件池中指定自定义事件类型的方法执行
const emit = function emit(type, ...params) {};
return {
on,
off,
emit
};
})();
细节完善:
let sub = (function () {
let pond = {};
// 向事件池中追加指定自定义事件类型的方法
const on = function on(type, func) {
// 每一次增加的时候,验证当前类型在事件池中是否已经存在
!Array.isArray(pond[type]) ? pond[type] = [] : null;//不存在就是undefined
let arr = pond[type];
if (arr.includes(func)) return;
arr.push(func);
};
// 从事件池中移除指定自定义事件类型的方法
const off = function off(type, func) {
let arr = pond[type],
i = 0,
item = null;
if (!Array.isArray(arr)) throw new TypeError(`${type} 自定义事件在事件池中并不存在!`);
for (; i < arr.length; i++) {
item = arr[i];
if (item === func) {
// 移除掉
// arr.splice(i, 1); //这样导致数据塌陷
arr[i] = null; //这样只是让集合中当前项值变为null,但是集合的机构是不发生改变的「索引不变」;下一次执行emit的时候,遇到当前项是null,我们再去把其移除掉即可;
break;
}
}
};
// 通知事件池中指定自定义事件类型的方法执行
const emit = function emit(type, ...params) {
let arr = pond[type],
i = 0,
item = null;
if (!Array.isArray(arr)) throw new TypeError(`${type} 自定义事件在事件池中并不存在!`);
for (; i < arr.length; i++) {//取出事件池中的所有事件执行
item = arr[i];
if (typeof item === "function") {
item(...params);
continue;
}
//不是函数的值都移除掉即可
arr.splice(i, 1);
i--;//防止数组塌陷
}
};
return {
on,
off,
emit
};
})();
测试:
const fn1 = () => console.log(1);
const fn2 = () => console.log(2);
const fn3 = () => {
console.log(3);
sub.off('A', fn1);
sub.off('A', fn2);
};
const fn4 = () => console.log(4);
const fn5 = () => console.log(5);
const fn6 = () => console.log(6);
sub.on('A', fn1);
sub.on('A', fn2);
sub.on('A', fn3);
sub.on('A', fn4);
sub.on('A', fn5);
sub.on('A', fn6);
setTimeout(() => {
sub.emit('A');
}, 1000);
setTimeout(() => {
sub.emit('A');
}, 2000);
sub.on('B', fn4);
sub.on('B', fn5);
sub.on('B', fn6);
setTimeout(() => {
sub.emit('B');
}, 3000);
off函数中使用arr.splice(i, 1)造成数据塌陷的原因:在使用splice的时候,会改变数组长度和索引。所以在for循环中用splice的时候,for循环中的i不会变,但是数组长度和索引变了,所以会导致跳过一些数组中某些item的遍历
浏览器中的事件绑定与发布订阅模式
DOM2事件绑定(addEventListener等)就是基于发布订阅模式来运行的。
DOM事件绑定的原理是给元素对象对应的事件行为的私有属性赋值。而DOM2事件绑定机制如下:
- 给当前元素的某一个事件行为,绑定多个不同的方法「事件池机制」
- 事件行为触发,会依次通知事件池中的方法执行
- 但是他只支持内置的标准事件,例如:click、dblclick、mouseenter.,我们没办法自定义事件
观察者模式
Observer 观察者模式也和发布订阅模式的思想差不多
- 定义观察者
observer:形式可以不一样,只需要观察者具备update方法即可,用来接收到消息的时候做出反应 - 定义目标
Subject:目标用来管理观察者,将具有update方法的观察者加入到观察者列表,observerList中 - 当目标发送信息(
notify)的时候,可以把消息传给观察者,观察者触发update方法即可
和发布订阅模式差不多。
代码如下:
// 定义观察者:形式可以不一样,只需要具备update方法即可
class OBSERVER {
update(msg) {
console.log(`我是观察者1,我接收到的消息是:${msg}`);
}
}
let DEMO = {
update(msg) {
console.log(`我是观察者2,我接收到的消息是:${msg}`);
}
};
// 目标
class Subject {
observerList = [];
add(observer) {
this.observerList.push(observer);
}
remove(observer) {
// 没有考虑塌陷问题
this.observerList = this.observerList.filter(item => item !== observer);
}
notify(...params) {
this.observerList.forEach(item => {
if (item && typeof item.update === "function") {
item.update(...params);
}
});
}
}
let sub = new Subject;
sub.add(new OBSERVER);
sub.add(DEMO);
setTimeout(() => {
sub.notify('hello world~~');
}, 1000);
构造器模式
Constructor构造器模式:站在面向对象的思想上去构建项目
- 自定义类和实例
- 私有&公有属性和方法
使用的地方:编写公共的类库 & 插件组件
function Fn() {
this.xxx = xxx;
}
Fn.prototype = {
constructor: Fn,
query() {},
// ...
};
Fn.xxx = function () {};
class Fn {
constructor() {
this.xxx = xxx;
}
query() {}
static xxx() {}
}
let f1 = new Fn;
let f2 = new Fn;
我们在使用某些插件时,每一次调用插件我们都是创造这个类的一个实例,既保证每个实例之间(每次调用之间)有自己的私有属性,互不影响,也可以保证一些属性方法还是公用的,有效避免代码的冗余
所以在我们自己写公共类库,或者插件组件的时候,基本都要用到构造器模式,即面向对象思想
工厂模式
简单的工厂模式:一个方法根据传递参数的不同,做了不同的处理。
function factory(options) {
if (options == null) options = {};
if (!/^(object|function)$/i.test(typeof options)) options = {};
let {
type,//类型
payload//值
} = options;
if (type === 'MYSQL') {
// ...
return;
}
if (type === 'SQLSERVER') {
// ...
return;
}
// ...
}
factory({
type: 'SQLSERVER',
payload: {
root: '',
pass: '',
select: ''
}
});
这样我们就可以根据传入的配置,进行不同的处理,就像工厂加工东西一样