JS设计模式之代理模式&发布订阅模式的拓展参考

概念:

设计模式的定义:

在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。

通俗说就是在某种场合下对某个问题的一种解决方案
-出自《JavsSctipt设计模式与开发实践》

设计模式的由来

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的

介绍两种常用的设计模式

代理模式

定义:

为一个对象找一个替代对象,以便对原对象进行访问

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都
有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演
出的细节和报酬都谈好之后,再把合同交给明星签。

代理模式的关键是,当应用端不方便直接访问一个对象或者不满足需要的时候,提供一个替身
对象来控制对这个对象的访问,应用端实际上访问的是替身对象。替身对象对请求做出一些处理之
后,再把请求转交给本体对象。

模式细分

虚拟代理(将开销大的运算延迟到需要时执行)
缓存代理(为开销大的运算结果提供缓存)
保护代理(黑白双簧,代理充当黑脸,拦截非分要求)
防火墙代理(控制网络资源的访问)
远程代理(为一个对象在不同的地址控件提供局部代表)
智能引用代理(访问对象执行一些附加操作)
写时复制代理(延迟对象复制过程,对象需要真正修改时才进行)

代理模式的变体种类非常多,限于其在js中的适用性还有个人的理解,这里只简单介绍一下几个常用的代理

保护代理

保护代理主要实现了访问主体的限制行为,代理B可以帮助A过滤掉一些请求。
代理对象可预先对请求进行处理,再决定是否转交给本体,代理和本体对外接口保持一致性

比如:

  1. 过滤敏感信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 主体,发送消息
function sendMsg(msg) {
console.log(msg)
}

// 代理,对消息进行过滤
function proxySendMsg(msg) {
// 无消息则直接返回
if (typeof msg === 'undefined') {
console.log('deny')
return
}

// 有消息则进行过滤
msg = ('' + msg).replace(/泥\s*煤/g, '')

sendMsg(msg)
}


sendMsg('泥煤呀泥 煤呀') // 泥煤呀泥 煤呀
proxySendMsg('泥煤呀泥 煤') // 呀
proxySendMsg() // deny
  1. 黑名单拦截 - 拒绝访问主体,保护代理的形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 例子:代理接听电话,实现拦截黑名单
var backPhoneList = ['189XXXXX140']; // 黑名单列表
// 代理
var ProxyAcceptPhone = function(phone) {
// 预处理
console.log('电话正在接入...');
if (backPhoneList.includes(phone)) {
// 屏蔽
console.log('屏蔽黑名单电话');
return
}
AcceptPhone.call(this, phone);
}
// 本体
var AcceptPhone = function(phone) {
console.log('接听电话:', phone);
};

// 外部调用代理
ProxyAcceptPhone('189XXXXX140');
ProxyAcceptPhone('189XXXXX141');

虚拟代理(延迟执行)

实例:

  1. 图片loading预加载
    在 Web 开发中,图片预加载是一种常用的技术,如果直接给某个 img标签节点设置 src属性,
    由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张
    loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img节点里。
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
var myImage = (function(){ 
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );

return {
setSrc: function( src ){
imgNode.src = src;
}
}
})();

var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
myImage.setSrc( './car_image.png' );
img.src = src;
}
}
})();

proxyImage.setSrc( 'http://pic1.win4000.com/wallpaper/d/58db13651c910.jpg' );

缓存代理(暂时存储)

缓存代理可以为一些开销大的运算结果提供暂时存储,在下次运算时,如果传递进来的参数和之前的一致,则可以直接返回前面存储的结果,而不是重新进行本体运算,减少本体调用次数。

例如,前后端分离,向后端请求分页的数据的时候,每次页码改变时都需要重新请求后端数据,我们可以将页面和对应的结果进行缓存,当请求同一页的时候,就不再请求后端的接口而是从缓存中去取数据。(这种场景也是有要求的,数据不会轻易变更的列表使用)

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
// 斐波那契数列
const getFibonacci = (number) => {
if (number <= 2) {
return 1;
} else {
return getFibonacci(number - 1) + getFibonacci(number - 2);
}
}

// 代理对象,创建缓存代理的工厂,参数fn可以为任意需要进行代理的函数,除了上述计算的本体对象函数外,还可以是进行其他操作的本体函数
const getCacheProxy = function(fn){
const resultCache = {};

return function(){
console.time('getFibonacciTime')
const args = Array.prototype.join.call(arguments, ',')
if(!(args in resultCache)){ //测试对象中是否有对应的name,有则直接返回该name的值
resultCache[args] = fn.apply(this, arguments)
}
console.timeEnd('getFibonacciTime')
return resultCache[args]
}
}

const getFibProxy = getCacheProxy(getFibonacci);
console.log(getFibProxy(40))
console.log(getFibProxy(41))
console.log(getFibProxy(42))
console.log(getFibProxy(43))

console.log(getFibProxy(40))
console.log(getFibProxy(41))
console.log(getFibProxy(42))
console.log(getFibProxy(43))
模式特点
  1. 代理对象可预先处理请求,再决定是否转交给本体
  2. 代理和本体对外显示接口保持一致性
  3. 代理对象仅对本体做一次包装
优缺点
优点
  • 可拦截和监听外部对本体对象的访问
  • 复杂运算前可以进行校验或资源管理
  • 对象职能粒度细分,函数功能复杂度降低,符合 “单一职责原则”
  • 依托代理,可额外添加扩展功能,而不修改本体对象,符合 “开发-封闭原则”
缺点
  • 额外代理对象的创建,增加部分内存开销
  • 处理请求速度可能有差别,非直接访问存在开销,但 “虚拟代理” 及 “缓存代理” 均能提升性能
总结

但是在业务开发时应该注意使用场景,不需要在编写对象时就去预先猜测是否需要使用代理模式,只有当对象的功能变得复杂或者我们需要进行一定的访问限制时,再考虑使用代理。

js中常用的代理模式有虚拟代理和缓存代理两种

注意:面向对象设计的原则——单一职责原则

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

就是对本体而言,功能要相对单一,便于拓展和代理的维护

发布订阅模式

定义:
发布 — 订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

之前通通曾经结合vue源码介绍过该设计模式的一些原理
所以在原理和结构方面不做详细的介绍,对该模式进行扩展的一个介绍

问大家一个问题,用发布订阅模式时,先发布还是先订阅?必须先订阅再发布吗?

我们所了解到的发布订阅模式,都是订阅者先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条信息,而在之前并没有对象来订阅它,这条消息无疑将会被当作垃圾回收掉。

在某些情况下,我们需要将这条信息保留下来,等到有对象来订阅它的时候,再重新把消息发布给订阅者。就如同QQ中的离线消息一样,离线信息被保存在服务器中,接收人下次登录上线之后,可以重新收到这条信息。

这种需求在实际项目中是存在的,比如在网站中,获取到用户信息之后才能渲染用户信息,而获取用户信息的操作是一个ajax异步请求。当aja请求成功返回之后会发布一个事件,在此之前订阅了此事件的用户导航模块可以接收到这些用户信息

但是这只是理想的状况,因为异步的原因,我们不能保证ajax请求返回的时间,有时候它返 回得比较快,而此时用户信息模块的代码还没有加载好(还没有订阅相应事件),特别是在用了 一些模块化惰性加载的技术后,这是很可能发生的事情。也许我们还需要一个方案,使得我们的 发布—订阅对象拥有先发布后订阅的能力。

为了满足这个需求,我们要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还
没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被 存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数, 也就是重新发布里面的事件。当然离线事件的生命周期只有一次,就像QQ的未读消息只会被重新阅读一次,所以刚才的操作我们只能进行一次。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

class Observer {
constructor() {
// 观察者
this._observer = {}
this._offlineStack = {} // 离线事件
}

_handlefflineStack (type) {
if(this._offlineStack[type]) {
const handlers = this._offlineStack[type]
this._offlineStack[type] = []
for (const handler of handlers) {
handler()
}
}
}

on (type, handler) {

if (!this._observer[type]) {
this._observer[type] = [];
}
this._observer[type].push(handler);

this._handlefflineStack(type)

return this
}
once (type, handler) {
const _self = this
function on () {
_self.off(type, on)
handler.apply(_self, arguments)
}
_self.on(type, on)
return _self
}
emit (type, ...args) {
const handlers = this._observer[type] || []
if(!handlers || !handlers.length) {
if (!this._offlineStack[type]) {
this._offlineStack[type] = [];
}
const _self = this
const fn = function () {
return _self.emit(type, ...args);
};
this._offlineStack[type].push(fn);
return
}
for (const handler of handlers) {
handler(...args)
}
}
off (type, handler) {
if (!this._observer[type]) {
return
}
if (!handler || typeof handler !== 'function') {
this._observer[type] = []
return
}
this._observer[type] = this._observer[type].filter(val => val !== handler)
}

}
const observer = new Observer()

observer.emit('click', 5, 6, 7)
setTimeout( ()=> {
observer.once('click', function () {
console.log(arguments)
})
observer.emit('click', 6564657)
observer.on('click', function () {
console.log(arguments)
})
}, 2000)
observer.emit('click', 2345, 7)

离线功能的问题:

  1. 发布出去的信息如果一直没有匹配到相应的订阅事件,会一直把未被订阅的信息保存着,占用内存
  2. 首先发布出去的信息,只能被第一次订阅的事件获取到,后面订阅的同样的事件,无法获取到之前已经发布且被订阅的信息。即未被订阅的发布出去的信息,仅能触发一次订阅

参考文献

  1. 《JavsSctipt设计模式与开发实践》

本文首发于个人技术博客 https://liliuzhu.gitee.io/blog

坚持技术分享,您的支持将鼓励我继续努力!
0%