设计模式

单例模式

单例模式就是保证一个类只有一个实例,并提供一个全局的访问点。

实现方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回。

单体模式有以下优点:

  • 用来划分命名空间,减少全局变量数量。
  • 使代码组织的更一致,提高代码阅读性和维护性。
  • 只能被实例化一次。

基本结构:

let Singleton = function (name){
    this.name = name;
    this.obj = null;
}
Singleton.prototype.getName = function(){
    return this.name;
}
function getObj(name){
    return this.obj || (this.obj = new Singleton(name));
}
let g1 = getObj('leo');
let g2 = getObj('pingan');
g1 === g2;    // true
g1 == g2;     // true
g1.getName(); // 'leo'
g2.getName(); // 'leo'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

弹窗实例:

let create = (() => {
    let div;
    return () => {
        if(!div){
            div = document.createElement('div');
            div.innderHTML = '我是leo创建的弹框';
            div.style.display = 'none';
            div.setAttribute("id", "leo");
            document.body.appendChild(div);
        }
        return div;
    }
})();
// 触发事件
document.getElementById('otherBtn').onclick = () => {
    let first = create();
    first.style.display = 'block';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

工厂模式

工厂模式的目的在于创建对象,实现下列目标:

  • 可重复执行,来创建相似对象;
  • 当编译时位置具体类型(类)时,为调用者提供一种创建对象的接口;
function Person(name, age, sex){
    let p = {}; // 或 let p = new Object(); 创建一个初始对象
    p.name = name;
    p.age = age;
    p.sex = sex;
    p.ask = function(){
        return 'my name is' + this.name;
    }
    return p;
}
let leo = new Person('leo', 18, 'boy');
let pingan = new Person('pingan', 18, 'boy');
console.log(leo.name, leo.age, leo.sex);          // 'leo', 18, 'boy'
console.log(pingan.name, pingan.age, pingan.sex); // 'pingan', 18, 'boy'
1
2
3
4
5
6
7
8
9
10
11
12
13
14

优点:

  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  • 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点:

每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。

代理模式

代理模式(Proxy Pattern) 为其他对象提供一种代理,来控制这个对象的访问,代理是在客户端和真实对象之间的介质。

优点

  • 职责单一且清晰。
  • 保护真实对象。
  • 开闭原则,高拓展性。

缺点

  • 由于在客户端和真实对象间添加代理对象,导致请求处理速度变慢。
  • 实现代理模式需要额外工作,有些代理模式实现起来非常复杂。
<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

适配器模式

适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。

class Plug {
  getName() {
    return '港版插头'
  }
}

class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 适配器转二脚插头'
  }
}

let target = new Target()
target.getName() // 港版插头 适配器转二脚插头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

装饰模式

装饰模式不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。

function readonly(target, key, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Test {
  constructor() {
    this.type = 'a'
  }
  @readonly
}

let t = new Test()
t.type = 'b' // 不可修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14

观察者模式和发布订阅模式

观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。

(例如vue内的自定义事件的Event Emitter,发布者完全不用感知到订阅者,事件的注册和触发都发生在事件总线上,实现了完全的解耦。)

观察者模式

为了实现松耦合

观察者模式里面,changed()方法所在的实例对象,就是被观察者(Subject,或者叫Observable),它只需维护一套观察者(Observer)的集合,这些Observer实现相同的接口,Subject只需要知道,通知Observer时,需要调用哪个统一方法就好了。

class Observer { // 观察者 有一个update方法,当观察者需要被触发的时候执行update
  constructor() {
    
  }
  update(val) {
    console.log('val');
  }
}
// 观察者列表
class ObserverList { // 这些观察者实现相同的接口
    constructor() {
        this.observerList = []
    }
    add(observer) {
        return this.observerList.push(observer);
    }
    remove(observer) {
        this.observerList = this.observerList.filter(ob => ob !== observer);
    }
    count() {
        return this.observerList.length;
    }
    get(index) {
        return this.observerList[index];
    }
}
class Subject { // 目标 只需维护一套观察者的集合 并知道通知观察者时调用哪个统一方法就好
  constructor() {
    this.observers = new ObserverList();
  }
  addObserver(observer) {
    this.observers.add(observer);
  }
  removeObserver(observer) {
    this.observers.remove(observer);
  }
  notify(...args) {
    let obCount = this.observers.count();
    for (let index = 0; index < obCount; index++) {
      this.observers.get(i).update(...args);
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

发布订阅模式

发布者,并不会直接通知订阅者,换句话说,发布者和订阅者,彼此互不相识。

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,会造成代码的冗余。而发布订阅模式则统一由调度中心处理,消除了发布者和订阅者之间的依赖。

class PubSub {
    constructor() {
        this.subscribers = {} // 事件中心
    }
    subscribe(type, fn) { // 往事件中心添加事件
        if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
          this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    }
    unsubscribe(type, fn) { // // 移除事件
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        this.subscribers[type] = listeners.filter(v => v !== fn);
    }
    publish(type, ...args) { // 触发事件中心对应事件
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        listeners.forEach(fn => fn(...args)); // 发布消息 => 通知所有订阅者       
    }
}

let ob = new PubSub();
ob.subscribe('add', (val) => console.log(val));
ob.publish('add', 1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

差异:在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

一个最小化vue响应式示例

class Watcher {
  update() {
    console.log('更新~'); // 组件更新啦
  }
}
// 这里的Dep就是一个观察者类,每一个data的属性都会有一个dep对象。当getter调用的时候,去dep里注册函数
class Dep { // 订阅者
  constructor() {
    this._watchers = new Set();
  }
  //在调用render的时候,就会需要用到data的属性值,此时会触发getter函数,将当前的Watcher函数注册进sub里。
  //2.当前组件相对应的Watcher对象都会被注册到该属性的Dep中。 -- 依赖收集。
  add(watcher) {
    if(!this._watchers.has(watcher)) {
      this._watchers.add(watcher);
    }
  }
  notify() {
    // 当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。
    this._watchers.forEach(watch => {
      watch.update();
    })
  }
}


// new一个自己的Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM。
Dep.target = new Watcher();// Dep.target暂时可以理解为一个全局变量,代表着的是当前正在渲染的Vue组件


// 组件初始化的时候,先给每一个Data属性都注册getter,setter,也就是reactive化。
function observer(target) {
  if (typeof target === 'object' && target !== null) {
    Object.keys(target).forEach(key => {
      defineReactive(target, key, target[key]);
    })
  }
}
function defineReactive(target, key, val) {
  const dep = new Dep();
  if (typeof val === 'object' && val !== null) {
    observer(val);
  }
  Object.defineProperty(target, key, {
    get() {
    // 1.每次render一个vue组件的时候,遍历所有属性,并添加当前watcher,直到收集完所有依赖属性的组件所对应的Watcher -- 订阅调用该属性的组件(将组件存放到一个订阅者数组中进行保存)
      dep.add(Dep.target);
      return val;
    },
    set(newVal) {
      dep.notify();
      val = newVal;
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55