前言
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
作者认为设计模式不应该脱离场景问题存在,设计模式应该是解决特定场景下的特定问题,提高代码的可复用性和可靠性。
本章会给大家介绍四种前端常用的设计模式,了解它们的适用场景以及解决的问题。四种常用设计模式:单例模式,工厂模式,代理(委托)模式,发布-订阅模式。其中在前端应用最广泛以及最重要的模式是发布-订阅模式模式(没有之一)。
设计模式源码
源码地址:https://github.com/dkypooh/front-end-develop-demo/tree/master/senior/design-pattern
工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
定义
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
实现方法
让其子类实现工厂接口,返回的也是一个抽象的产品。
适用场景
子类不需要定义父类的实现,只需要实现父类定义接口。
- 您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
- Hibernate 换数据库只需换方言和驱动就可以。
代码实例
// 1. 父类实现基本定义,定义姓名
class Parent {
getName() {
}
}
// 2. 子类集成了父类的各个基因,男孩名字叫 bob
class ChildBoy extends Parent {
getName() {
return 'bob';
}
}
// 3. 子类集成了父类的各个基因,女孩名字叫 alice
class ChildGirl extends Parent {
getName() {
return 'alice';
}
}
单例模式
定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
实现方法
判断系统是否已经有这个单例,如果有则返回,如果没有则创建,确保了一个类只有一个实例对象。
适用场景
当您想控制实例数目,节省系统资源的时候,有如下场景可以适用:
- 全局性实例化组件,例如:
Toast组件
,Modal弹窗组件
- 避免类防止多次创建实例的场景,例如:实例化事件模块
new EventEmitter()
代码实例
// 1. 创建 Toast 单例类
class Singleton {
constructor(options) {
this.options = options
}
show(message) {
alert(message)
}
}
// 2. 创建代理类,确保构造器只有一个实例
function ProxyClass() {
let instance = null
return function(options) {
if (!instance) {
instance = new Singleton(options);
}
return instance;
}
}
// 3. 执行代理函数,闭包保存实例,返回单例类
const SingletonClass = ProxyClass();
// 4. 测试代码,实例化两个类,实例是否相同
const d = new SingletonClass('dd');
const c = new SingletonClass('cc');
d === c
事件代理模式
事件代理模式在前端的主要应用场景是事件委托(event delegate)。
定义
JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
实现方法
一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
适用场景
事件冒泡 和 事件捕获 分别由 微软 和 网景 公司提出,后来 W3C 将两者结合,制定了统一的标准 —— 先捕获再冒泡。
为了更好的理解事件流模型,我们把 DOM 树想象成一个靶子,父节点在外,子节点在内。如下图所示:
- 事件冒泡(event bubbling) 由内向外,即从 DOM 树的子到父,
div -> body -> html -> document
- 事件捕获(event capturing) 由外向内,即从 DOM 树的父到子,
document -> html -> body -> div
代码实例
在 JavaScript 中,addEventListener
方法用于向指定元素添加事件句柄。
语法:element.addEventListener(event, function, useCapture)
element | 目标元素 | |
---|---|---|
event | 事件名,如 click | |
function | 事件触发时执行的函数 | |
useCapture | Bool值,true - 事件句柄在 捕获 阶段执行,false- false- 默认。事件句柄在 冒泡 阶段执行 |
设置 addEventListener
捕获方式为 false
冒泡方式
/**.html**/
<div class="t3">document
<div class="t2">html
<div class="t1">body
<div class="t0">div</div>
</div>
</div>
</div>
/**.js**/
var $t0 = document.getElementsByClassName('t0')[0];
var $t1 = document.getElementsByClassName('t1')[0];
var $t2 = document.getElementsByClassName('t2')[0];
var $t3 = document.getElementsByClassName('t3')[0];
$t0.addEventListener("click", function(){
alert("click div")
}, false);
$t1.addEventListener("click", function(){
alert("click body")
}, false);
$t2.addEventListener("click", function(){
alert("click html")
}, false);
$t3.addEventListener("click", function(){
alert("click document")
}, false);
发布-订阅模式
定义
观察者模式中的源(Subject)就像一个发布者(Publisher),观察者(Observer)完全和订阅者(Subscriber)关联。
在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,需要有个一个中间媒介做集中化处理。
上图解释:一个发布者发布一条消息,通过订阅-发布模型,可以被多的接收者收到。
观察者模式 VS 发布-订阅模式
读者开始会对这两者模式差异存在疑惑,作者认为在前端领域,这两种存在一些细微差别,使用功能来说可以等同。我们同样可以成为观察者模式 为 发布-订阅模式。
它们细小的差别在于,发布-订阅模式相比较于观察者模式,有一个中心管控(或者中介者)的模块。如下图所示:
应用场景
在前端发布-订阅模式,又称为观察者模式。它主要有如下几方面的应用场景。
- 保持组件数据通信的扁平化,例如不同根节点子组件通信可以使用发布-订阅模式
- 数据通信解耦,在统一内存环境下都可以实现不同模块之间的通信
源码实现
DOM原生支持的 CustomEvent
也是一个发布-订阅模型,下面使用此 DOM API 使用此模型。
<body>
<div id="tap"> Sub/Pub </div>
<script>
// 1. 获取id 为 tap DOM元素节点
const node = document.getElementById('tap');
// 2. 添加tap节点自定义事件
node.addEventListener('cat', (result) => {
console.log(result.detail);
})
// 3. 绑定tap节点 click 事件
node.addEventListener('click', () => {
// 3.1 定义自定义事件
const event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}})
// 3.2 节点出发事件
node.dispatchEvent(event);
})
</script>
</body>
EventEmitter实现
设计模式章节的最后, 和作者一起通过 Typescript 实现一个 EventEmitter 类, EventEmitter 实现两个核心方法: emit
(发布事件) 和 on
(订阅事件)。
EventEmitter 实现的基本思想:使用Map来维护事件名称和事件方法的映射关系。同时通过 emit
绑定当前作用域,执行方法。
代码实例
源码参考地址:https://github.com/dkypooh/front-end-develop-demo/blob/master/senior/sdk/src/event.ts
export default class EventEmitter {
// 1. 创建一个eventMap,用于存储事件名和函数的映射关系
private _eventMap = new Map();
/**
* on
* @param {String} type 事件名称
* @param {Function} fn 绑定函数
*/
on(type: string, fn: ICallback) {
// 2. 实现 on 事件监听方法
// 2.1 如果 eventMap 有此方法,则维护多个方法,push到方法数组中, emit的时候批量执行
// 2.2 如果 enentMap 没有此方法, 则 set 这个事件函数
if (this._eventMap.has(type)) {
const cbs = this._eventMap.get(type);
cbs.push(fn);
} else {
this._eventMap.set(type, [fn]);
}
return this;
}
/**
* emit
* @param {String} type 事件名称
* @param args
*/
emit(type: string, ...args: any[]): boolean {
// 3. 实现 emit 事件订阅方法
// 3.1 如果 event 中有此方法,则批量执行,同时通过Apply 绑定当前作用域执行。
if (this._eventMap.has(type)) {
const cbs = this._eventMap.get(type);
for (const fn of cbs) {
fn.apply(this, args);
}
return true;
} else {
return false;
}
}
}
代码详解如下:
- 创建一个eventMap,用于存储事件名和函数的映射关系
- 实现 on 事件监听方法,如果 eventMap 有此方法,则维护多个方法,push到方法数组中, emit的时候批量执行,如果 enentMap 没有此方法, 则 set 这个事件函数
- 实现 emit 事件订阅方法,如果 event 中有此方法,则批量执行,同时通过Apply 绑定当前作用域执行。
结语
设计模式的选择, 必须是根据不同场景问题出发,解决不同场景的实际问题。 上文介绍了前端开发中常用的四种设计模式。
最后带大家实现一个 EventEmitter 事件发布-订阅类(前端最常用的工具类),读者可以体会其中的实现思想,同时可以使用此类解耦项目数据通信。
思考题
Q:如何实现一个类的单例,防止多次初始化?