代理模式这个设计模式对我来说有特殊的意义,因为这是我在工作中第一次分享学习的主题,当时我还是一个实习生,现在一眨眼已经过去很多年了。

设计模式是什么

当时分享的具体内容是什么我已经不记得了,而且其实我当时也并不太理解设计模式是什么,后来阅读了一些文章,我隐隐约约地理解设计模式就是代码的设计方案。

说到这,我想起之前看的《黑客与画家》,里面说优秀的程序员更像画家,这么一来不就对上了吗,两者关注的都是如何设计,是创造者,画家注意的是如何去设计构图,程序员注意的是如何去设计数据结构、代码结构、系统架构,一份设计良好的程序代码具有更好的可读性和可维护性,所以设计模式就是一些经过检验的、通用的、可复用的代码设计方案。

设计模式分类

wiki中将设计模式分为四类,分别是:

  • 创建模式(creational patterns)
  • 结构模式(structural patterns)
  • 行为模式(behavioral patterns)
  • 并发模式(concurrency patterns)

代理模式属于其中的结构模式。

代理

说到代理,我们可以将其对应到一个很生活化的词汇——中介,比如房产中介、留学中介等等一些中介机构,就是原本要直接建立关系的双方a和b之间多了一个中间人c,这个c就是代理;甚至我们可以就从字面上理解,代理作为动词是代为处理,作为名词就是代为处理的机构;代理可以为a处理事情,也可以为b处理事情。

当我们的代码中增加了具有代理功能的角色,就可以认为其应用了代理模式。比如ES6中新增的Proxy

// 假设存在一个对象a
let a = {
  name: '鸡蛋'
}
// 但是我不希望代码里的其他部分对a直接进行访问
// 此时我们就可以创建一个a的代理,来处理其他内容对a的访问
let aProxy = new Proxy(a, { 
  get: function (a, key) { 
    return Reflect.get(a, key);
  } 
});

生活情境

那么什么情况下会使用到代理呢?以下罗列我想到的一些生活情境:

第一个,a想要与b建立关系,但是没有渠道、联系不上,刚好c可以接触到b,c就可以替a去联系b

第二个,a想要与b联系,但是又不想b知道自己,也可以使用代理c代替自己与b联系

第三个,a想要去b国留学,但是除了提交申请,对其他的流程不甚了解,就可以通过中介机构c提交申请并处理其他事宜

第四个,b作为一个重要资源,a不能随便访问,需要通过b的代理c来校验a的身份和权限,通过验证后c可以将资源b中符合a权限范围的内容展示给a

总结一下,代理在上述情境中起到的作用大致是:

  • 建立渠道
  • 保护信息
  • 处理额外事项

解决的问题

现在我们来看wiki中描述的代理模式所解决的问题:

What problems can the Proxy design pattern solve?

  • The access to an object should be controlled.
  • Additional functionality should be provided when accessing an object.

When accessing sensitive objects, for example, it should be possible to check that clients have the needed access rights.

翻译过来大概是以下意思:

  • 对受控对象的访问
  • 访问对象时应提供附加的功能

怎么做

那么软件设计中的代理模式具体是怎么做的呢?wiki也给出了描述:

Define a separate Proxy object that

  • can be used as substitute for another object (Subject) and
  • implements additional functionality to control the access to this subject.

This makes it possible to work through a Proxy object to perform additional functionality when accessing a subject. For example, to check the access rights of clients accessing a sensitive object.

To act as substitute for a subject, a proxy must implement the Subject interface. Clients can't tell whether they work with a subject or its proxy.

翻译过来大概是以下意思:

定义一个单独的代理对象

  • 可用于替代另一个对象(主体)
  • 并实现额外功能,以控制对该主体的访问。

这样就可以通过代理对象在访问主体时执行附加功能。例如,检查访问敏感对象的客户端的访问权限。

要替代主体,代理必须实现主体接口。客户无法分辨他们是在与主体还是其代理一起工作。

应用场景

既然设计模式是通用的解决方案,那必然有其应用场景。

1. wiki

以下是wiki给出的代理模式三个可能的应用场景

远程代理

在分布式对象通信中,本地对象代表远程对象(属于不同地址空间的对象)。本地对象是远程对象的代理,对本地对象的方法调用会导致对远程对象的远程方法调用。一个例子是自动取款机的实现,自动取款机可能持有远程服务器中银行信息的代理对象。

虚拟代理

在某些情况下,骨架表示可能比复杂或笨重的对象更有优势。当底层图像体积庞大时,可以使用虚拟代理对象来表示,并根据需要加载真实对象。

保护代理

保护代理可用于根据访问权限 控制对资源的访问。

2. 前端应用

那在前端有哪些场景可以应用代理模式呢?

虚拟代理

首先就是可以将虚拟代理应用在图片懒加载,这也是性能优化的一种手段。

在图片较大或者较多的情况下(列表)应用虚拟代理,具体操作就是,使用占位元素代替图片渲染,等待图片加载完毕或进入可视区域后,再进行真实图片的渲染。这里就是用占位元素作为真实图片的代理,以控制在图片未加载完成时如何去处理其渲染。

事件代理

比如利用事件冒泡,将事件监听器放置在更上级的元素,来实现事件代理。

我们知道DOM事件流有三个阶段:事件捕获 => 到达目标 => 事件冒泡,当我们想处理某个目标元素上的事件,可以在事件流到达目标元素时处理,也可以在事件流从目标元素冒泡到更上级的元素时进行处理。

使用事件代理,就相当于更上级的元素代替目标元素去处理事件,而不是目标元素直接去处理事件。

这在一些情况下,可以提高代码的性能。比如,要监听li上的点击事件:

<ul id="father">
        <li><a href="#">链接1号</a></li>
        <li><a href="#">链接2号</a></li>
        <li><a href="#">链接3号</a></li>
        <li><a href="#">链接4号</a></li>
        <li><a href="#">链接5号</a></li>
        <li><a href="#">链接6号</a></li>
</div>

假如我们给每个li都设置监听器,那至少要加6个监听器,如果标签进一步增多,那么性能开销会加大;而如果li支持动态添加,那就需要每增加一个li就得绑定一次事件。

此时使用事件代理就可以很大程度上提升代码的性能,只需要在ul上绑定一次事件就足矣;而且通常在列表中,点击每个列表项的事件处理逻辑往往差不多,只需在事件处理程序中使用id之类的属性对li进行区分即可。

再比如接口请求事件,前端如果使用axios请求接口,可以在axios的拦截器中先去校验本地是否存在token等认证信息,此时可以把这个axios看作一个代理,它代替我们去处理请求事件,在帮我们做了一系列校验以及格式处理等操作后,才会发起请求。

保护代理

也是对资源的访问控制,比如前端路由跳转,可以在路由守卫中做校验,是否具有目标路由的访问权限,如果有权限,才能进行跳转,此时可以把路由守卫看作一个代理。

以上可以看作是对目标对象(目标路由)的保护。

另外我们也可以使用ES6中的Proxy来代理真实的对象,以防止真实对象被意外访问或修改,当然也可以在通过代理对象来访问真实对象时,做一些额外的操作。

缓存代理

在一些场景下,我们可以把缓存也当作一种代理,比如vue中的计算属性,计算属性通常是根据普通属性计算而来,在普通属性没有更新的情况下也去计算,就有点浪费性能了,所以计算属性将最近一次的计算结果进行缓存,在没有更新的情况下,就可以将这个计算结果当做计算属性的一个代理。

总结

使用代理模式可以达到加强控制、提升性能、优化代码结构等效果。

之所以它在分类中属于结构模式,也很好理解,就是在a和b之间多了一个c,代码结构发生了变化,但a和b的行为都没有变,并且没有创建新的对象,c属于媒介而不是对象。

参考资料

wiki: Software_design_pattern

wiki: Proxy pattern


beckyyyy
550 声望414 粉丝

工作多年的一只前端菜鸟