设计模式
单例模式
单例模式就是保证一个类只有一个实例,并提供一个全局的访问点。
实现方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回。
单体模式有以下优点:
- 用来划分命名空间,减少全局变量数量。
- 使代码组织的更一致,提高代码阅读性和维护性。
- 只能被实例化一次。
基本结构:
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'
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';
}
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'
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>
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() // 港版插头 适配器转二脚插头
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' // 不可修改
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);
}
}
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);
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;
}
})
}
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