前端进阶能力 - 设计模式 - 图1

前言

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。

作者认为设计模式不应该脱离场景问题存在,设计模式应该是解决特定场景下的特定问题,提高代码的可复用性和可靠性。

本章会给大家介绍四种前端常用的设计模式,了解它们的适用场景以及解决的问题。四种常用设计模式:单例模式,工厂模式,代理(委托)模式,发布-订阅模式。其中在前端应用最广泛以及最重要的模式是发布-订阅模式模式(没有之一)。

设计模式源码

源码地址:https://github.com/dkypooh/front-end-develop-demo/tree/master/senior/design-pattern

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

定义

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

实现方法

让其子类实现工厂接口,返回的也是一个抽象的产品。

适用场景

子类不需要定义父类的实现,只需要实现父类定义接口。

  1. 您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
  2. Hibernate 换数据库只需换方言和驱动就可以。

代码实例

  1. // 1. 父类实现基本定义,定义姓名
  2. class Parent {
  3. getName() {
  4. }
  5. }
  6. // 2. 子类集成了父类的各个基因,男孩名字叫 bob
  7. class ChildBoy extends Parent {
  8. getName() {
  9. return 'bob';
  10. }
  11. }
  12. // 3. 子类集成了父类的各个基因,女孩名字叫 alice
  13. class ChildGirl extends Parent {
  14. getName() {
  15. return 'alice';
  16. }
  17. }

单例模式

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

实现方法

判断系统是否已经有这个单例,如果有则返回,如果没有则创建,确保了一个类只有一个实例对象。

适用场景

当您想控制实例数目,节省系统资源的时候,有如下场景可以适用:

  1. 全局性实例化组件,例如:Toast组件Modal弹窗组件
  2. 避免类防止多次创建实例的场景,例如:实例化事件模块new EventEmitter()

代码实例

  1. // 1. 创建 Toast 单例类
  2. class Singleton {
  3. constructor(options) {
  4. this.options = options
  5. }
  6. show(message) {
  7. alert(message)
  8. }
  9. }
  10. // 2. 创建代理类,确保构造器只有一个实例
  11. function ProxyClass() {
  12. let instance = null
  13. return function(options) {
  14. if (!instance) {
  15. instance = new Singleton(options);
  16. }
  17. return instance;
  18. }
  19. }
  20. // 3. 执行代理函数,闭包保存实例,返回单例类
  21. const SingletonClass = ProxyClass();
  22. // 4. 测试代码,实例化两个类,实例是否相同
  23. const d = new SingletonClass('dd');
  24. const c = new SingletonClass('cc');
  25. d === c

事件代理模式

事件代理模式在前端的主要应用场景是事件委托(event delegate)。

定义

JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

实现方法

一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

适用场景

前端进阶能力 - 设计模式 - 图2

事件冒泡 和 事件捕获 分别由 微软 和 网景 公司提出,后来 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 冒泡方式

  1. /**.html**/
  2. <div class="t3">document
  3. <div class="t2">html
  4. <div class="t1">body
  5. <div class="t0">div</div>
  6. </div>
  7. </div>
  8. </div>
  9. /**.js**/
  10. var $t0 = document.getElementsByClassName('t0')[0];
  11. var $t1 = document.getElementsByClassName('t1')[0];
  12. var $t2 = document.getElementsByClassName('t2')[0];
  13. var $t3 = document.getElementsByClassName('t3')[0];
  14. $t0.addEventListener("click", function(){
  15. alert("click div")
  16. }, false);
  17. $t1.addEventListener("click", function(){
  18. alert("click body")
  19. }, false);
  20. $t2.addEventListener("click", function(){
  21. alert("click html")
  22. }, false);
  23. $t3.addEventListener("click", function(){
  24. alert("click document")
  25. }, false);

发布-订阅模式

定义

观察者模式中的源(Subject)就像一个发布者(Publisher),观察者(Observer)完全和订阅者(Subscriber)关联。

在发布-订阅模式,消息的发送方,叫做发布者(publishers),消息不会直接发送给特定的接收者,需要有个一个中间媒介做集中化处理。

前端进阶能力 - 设计模式 - 图3
上图解释:一个发布者发布一条消息,通过订阅-发布模型,可以被多的接收者收到。

观察者模式 VS 发布-订阅模式

读者开始会对这两者模式差异存在疑惑,作者认为在前端领域,这两种存在一些细微差别,使用功能来说可以等同。我们同样可以成为观察者模式 为 发布-订阅模式。
它们细小的差别在于,发布-订阅模式相比较于观察者模式,有一个中心管控(或者中介者)的模块。如下图所示:
前端进阶能力 - 设计模式 - 图4

应用场景

在前端发布-订阅模式,又称为观察者模式。它主要有如下几方面的应用场景。

  1. 保持组件数据通信的扁平化,例如不同根节点子组件通信可以使用发布-订阅模式
  2. 数据通信解耦,在统一内存环境下都可以实现不同模块之间的通信

源码实现

DOM原生支持的 CustomEvent 也是一个发布-订阅模型,下面使用此 DOM API 使用此模型。

  1. <body>
  2. <div id="tap"> Sub/Pub </div>
  3. <script>
  4. // 1. 获取id 为 tap DOM元素节点
  5. const node = document.getElementById('tap');
  6. // 2. 添加tap节点自定义事件
  7. node.addEventListener('cat', (result) => {
  8. console.log(result.detail);
  9. })
  10. // 3. 绑定tap节点 click 事件
  11. node.addEventListener('click', () => {
  12. // 3.1 定义自定义事件
  13. const event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}})
  14. // 3.2 节点出发事件
  15. node.dispatchEvent(event);
  16. })
  17. </script>
  18. </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

  1. export default class EventEmitter {
  2. // 1. 创建一个eventMap,用于存储事件名和函数的映射关系
  3. private _eventMap = new Map();
  4. /**
  5. * on
  6. * @param {String} type 事件名称
  7. * @param {Function} fn 绑定函数
  8. */
  9. on(type: string, fn: ICallback) {
  10. // 2. 实现 on 事件监听方法
  11. // 2.1 如果 eventMap 有此方法,则维护多个方法,push到方法数组中, emit的时候批量执行
  12. // 2.2 如果 enentMap 没有此方法, 则 set 这个事件函数
  13. if (this._eventMap.has(type)) {
  14. const cbs = this._eventMap.get(type);
  15. cbs.push(fn);
  16. } else {
  17. this._eventMap.set(type, [fn]);
  18. }
  19. return this;
  20. }
  21. /**
  22. * emit
  23. * @param {String} type 事件名称
  24. * @param args
  25. */
  26. emit(type: string, ...args: any[]): boolean {
  27. // 3. 实现 emit 事件订阅方法
  28. // 3.1 如果 event 中有此方法,则批量执行,同时通过Apply 绑定当前作用域执行。
  29. if (this._eventMap.has(type)) {
  30. const cbs = this._eventMap.get(type);
  31. for (const fn of cbs) {
  32. fn.apply(this, args);
  33. }
  34. return true;
  35. } else {
  36. return false;
  37. }
  38. }
  39. }

代码详解如下:

  1. 创建一个eventMap,用于存储事件名和函数的映射关系
  2. 实现 on 事件监听方法,如果 eventMap 有此方法,则维护多个方法,push到方法数组中, emit的时候批量执行,如果 enentMap 没有此方法, 则 set 这个事件函数
  3. 实现 emit 事件订阅方法,如果 event 中有此方法,则批量执行,同时通过Apply 绑定当前作用域执行。

结语

设计模式的选择, 必须是根据不同场景问题出发,解决不同场景的实际问题。 上文介绍了前端开发中常用的四种设计模式。

最后带大家实现一个 EventEmitter 事件发布-订阅类(前端最常用的工具类),读者可以体会其中的实现思想,同时可以使用此类解耦项目数据通信。

思考题

Q:如何实现一个类的单例,防止多次初始化?