热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

学习JavaScript设计模式第十一篇:深入理解代理模式

代理模式(ProxyPattern)又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。代理模式把代理对

代理模式(Proxy Pattern)又称委托模式,它为目标对象创造了一个代理对象,以控制对目标对象的访问。

代理模式把代理对象插入到访问者和目标对象之间,从而为访问者对目标对象的访问引入一定的间接性。正是这种间接性,给了代理对象很多操作空间,比如在调用目标对象前和调用后进行一些预操作和后操作,从而实现新的功能或者扩展目标的功能。

1. 你曾见过的代理模式

明星总是有个助理,或者说经纪人,如果某导演来请这个明星演出,或者某个品牌来找明星做广告,需要经纪人帮明星做接洽工作。而且经纪人也起到过滤的作用,毕竟明星也不是什么电影和广告都会接。类似的场景还有很多,再比如领导和秘书…(emmm)。

再看另一个例子。打官司是件非常麻烦的事,包括查找法律条文、起草法律文书、法庭辩论、签署法律文件、申请法院执行等等流程。此时,当事人就可聘请代理律师来完成整个打官司的所有事务。当事人只需与代理律师签订全权委托协议,那么整个打官司的过程,当事人都可以不用出现。法院的一些复杂事务都可以通过代理律师来完成,而法院需要当事人完成某些工作的时候,比如出庭,代理律师才会通知当事人,并为当事人出谋划策。

在类似的场景中,有以下特点:

1. 导演/法院(访问者)对明星/当事人(目标)的访问都是通过经纪人/律师(代理)来完成;

2. 经纪人/律师(代理)对访问有过滤的功能;


2. 实例的代码实现

// 明星
var SuperStar = {name: '小鲜肉',playAdvertisement: function(ad) {console.log(ad)}
}// 经纪人
var ProxyAssistant = {name: '经纪人张某',playAdvertisement: function(reward, ad) {// 如果报酬超过100Wif (reward > 1000000) { console.log('没问题,我们小鲜肉最喜欢拍广告了!');SuperStar.playAdvertisement(ad);} else{console.log('没空,滚!');}}
}ProxyAssistant.playAdvertisement(10000, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没空,滚

这里我们通过经纪人的方式来和明星取得联系,经纪人会视条件过滤一部分合作请求。

我们可以升级一下,比如如果明星没有档期的话,可以通过经纪人安排档期,当明星有空的时候才让明星来拍广告。这里通过 Promise 的方式来实现档期的安排:

// 明星
const SuperStar = {name: '小鲜肉',playAdvertisement(ad) {console.log(ad);}
}// 经纪人
const ProxyAssistant = {name: '经纪人',scheduleTime() {return new Promise((resolve, reject) => {setTimeout(() => {console.log('小鲜肉有空了');// 发现明星有空了resolve();}, 2000) })},playAdvertisement(reward, ad) {// 如果报酬超过100Wif (reward > 1000000) { console.log('没问题,我们小鲜肉最喜欢拍广告了')// 安排上了ProxyAssistant.scheduleTime().then(() => SuperStar.playAdvertisement(ad))} else{console.log('没空,滚');}}
}ProxyAssistant.playAdvertisement(10000, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没空,滚ProxyAssistant.playAdvertisement(1000001, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没问题,我们小鲜肉最喜欢拍广告了
// 小鲜肉有空了
// 纯蒸酸牛奶,味道纯纯,尽享纯蒸

这里就简单实现了经纪人对请求的过滤,对明星档期的安排,实现了一个代理对象的基本功能。


3. 代理模式的概念

对于上面的例子,明星就相当于被代理的目标对象(Target),而经纪人就相当于代理对象(Proxy),希望找明星的人是访问者(Visitor),他们直接找不到明星,只能找明星的经纪人来进行业务商洽。主要有以下几个概念:

1. Target:目标对象,也是被代理对象,是具体业务的实际执行者;

2. Proxy: 代理对象,负责引用目标对象,以及对访问的过滤和预处理;

ES6 原生提供了  Proxy 构造函数,这个构造函数让我们可以很方便地创建代理对象:

var proxy = new Proxy(target, handler);

参数中 target 是被代理对象,handler 用来设置代理行为。

这里使用 Proxy 来实现一下上面的经纪人例子:

// 明星
const SuperStar = {name: '小鲜肉',// 档期标识位,false-没空,true-有空scheduleFlag: false, playAdvertisement(ad) {console.log(ad)}
}// 经纪人
const ProxyAssistant = {name: '经纪人',scheduleTime(ad) {// 在这里监听 scheduleFlag 值的变化const schedule = new Proxy(SuperStar, { set(obj, prop, val) {if (prop !== 'scheduleFlag') {return};// 小鲜肉现在有空了if (obj.scheduleFlag === false && val === true) { obj.scheduleFlag = true;// 安排上了obj.playAdvertisement(ad); }}});setTimeout(() => {console.log('小鲜鲜有空了');// 明星有空了schedule.scheduleFlag = true }, 2000)},playAdvertisement(reward, ad) {// 如果报酬超过100Wif (reward > 1000000) { console.log('没问题,我们小鲜肉最喜欢拍广告了')ProxyAssistant.scheduleTime(ad);} else{console.log('没空,滚!');}}
}ProxyAssistant.playAdvertisement(10000, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没空,滚
ProxyAssistant.playAdvertisement(1000001, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没问题,我们小鲜肉最喜欢拍广告了
// 小鲜肉有空了
// 纯蒸酸牛奶,味道纯纯,尽享纯蒸

在 ES6 之前,一般是使用 Object.defineProperty 来完成相同的功能,我们可以使用这个 API 改造一下:

// 明星
const SuperStar = {name: '小鲜肉',// 档期标识位,false-没空,true-有空scheduleFlag: false, playAdvertisement(ad) {console.log(ad);}
}// 经纪人
const ProxyAssistant = {name: '经纪人',scheduleTime(ad) {// 在这里监听 scheduleFlag 值的变化Object.defineProperty(SuperStar, 'scheduleFlag', { get() {return SuperStar.scheduleFlag},set(val) {if (SuperStar.scheduleFlag === false && val === true) { // 小鲜肉现在有空了 SuperStar.scheduleFlag = true;// 安排上了SuperStar.playAdvertisement(ad); }}});setTimeout(() => {console.log('小鲜肉有空了');SuperStar.scheduleFlag = true;}, 2000) },playAdvertisement(reward, ad) {// 如果报酬超过100Wif (reward > 1000000) { console.log('没问题,我们小鲜肉最喜欢拍广告了');ProxyAssistant.scheduleTime(ad);} else{console.log('没空,滚');}}
}ProxyAssistant.playAdvertisement(10000, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没空,滚ProxyAssistant.playAdvertisement(1000001, '纯蒸酸牛奶,味道纯纯,尽享纯蒸');
// 没问题,我们小鲜肉最喜欢拍广告了
// 小鲜肉有空了
// 纯蒸酸牛奶,味道纯纯,尽享纯蒸

4. 代理模式在实战中的应用


4.1. 拦截器

使用代理模式代理对象的访问的方式,一般又被称为拦截器。

拦截器的思想在实战中应用非常多,比如我们在项目中经常使用 Axios 的实例来进行 HTTP 的请求,使用拦截器 interceptor 可以提前对 request 请求和 response 返回进行一些预处理,比如:

1. request 请求头的设置,和 COOKIE 信息的设置;

2. 权限信息的预处理,常见的比如验权操作或者 Token 验证;

3. 数据格式的格式化,比如对组件绑定的 Date 类型的数据在请求前进行一些格式约定好的序列化操作;

4. 空字段的格式预处理,根据后端进行一些过滤操作;

5. response 的一些通用报错处理,比如使用 Message 控件抛出错误;

除了 HTTP 相关的拦截器之外,还有 vue-router、react-router 路由跳转的拦截器,可以进行一些路由跳转的预处理等操作。以 vue-router 的路由全局前置守卫为例:

const router = new VueRouter({ ... })router.beforeEach((to, from, next) => {console.log('beforeRouteEnter');next();
})

也许你会有疑问,拦截器看起来似乎和装饰者模式很像,但是要注意装饰者模式和代理模式的区别,代理模式控制访问者对目标对象的访问,而装饰者模式只给目标对象添加功能,原有功能不变且可直接使用。Axios 拦截器是可以取消请求的,vue-router 路由拦截器也可以进行路由截停和重定向等等复杂操作,这些场景下,无疑是代理模式,因为这里的拦截器控制了对目标对象的访问,如果没有进行访问控制而只进行消息预处理和后处理,那么则可以当作是装饰者模式。

4.2. 前端框架的数据响应式化

现在的很多前端框架或者状态管理框架都使用上面介绍的 Object.defineProperty 和 Proxy 来实现数据的响应式化,比如 Vue、Mobx、AvalonJS 等,Vue 2.x 与 AvalonJS 使用前者,而 Vue 3.x 与 Mobx 5.x 使用后者。

Vue 2.x 中通过 Object.defineProperty 来劫持各个属性的 setter/getter,在数据变动时,通过发布-订阅模式发布消息给订阅者,触发相应的监听回调,从而实现数据的响应式化,也就是数据到视图的双向绑定。

为什么 Vue 2.x 到 3.x 要从 Object.defineProperty 改用 Proxy 呢,是因为前者的一些局限性,导致的以下缺陷:

1. 无法监听利用索引直接设置数组的一个项,例如:vm.items[indexOfItem] = newValue;

2. 无法监听数组的长度的修改,例如:vm.items.length = newLength;

3. 无法监听 ES6 的 Set、WeakSet、Map、WeakMap 的变化;

4. 无法监听 Class 类型的数据;

5. 无法监听对象属性的新增或者删除;

除此之外还有性能上的差异,基于这些原因,Vue 3.x 改用 Proxy 来实现数据监听了。当然缺点就是对 IE 用户的不友好,兼容性敏感的场景需要做一些取舍。

4.3. 缓存代理

在高阶函数的文章中,就介绍了备忘模式,备忘模式就是使用缓存代理的思想,将复杂计算的结果缓存起来,下次传参一致时直接返回之前缓存的计算结果。

4.4. 保护代理和虚拟代理

有的书籍中着重强调代理的两种形式:保护代理和虚拟代理:

1. 保护代理 :当一个对象可能会收到大量请求时,可以设置保护代理,通过一些条件判断对请求进行过滤;

2. 虚拟代理 :在程序中可以能有一些代价昂贵的操作,此时可以设置虚拟代理,虚拟代理会在适合的时候才执行操作。

保护代理其实就是对访问的过滤,之前的经纪人例子就属于这种类型。

而虚拟代理是为一个开销很大的操作先占位,之后再执行,比如:

1. 一个很大的图片加载前,一般使用菊花图、低质量图片等提前占位,优化图片加载导致白屏的情况;

2. 现在很流行的页面加载前使用 骨架屏 来提前占位,很多 WebApp 和 NativeApp 都采用这种方式来优化用户白屏体验;

图片描述

4.5. 正向代理与反向代理

还有个经常用的例子是反向代理(Reverse Proxy),反向代理对应的是正向代理(Forward Proxy),他们的区别是:

1. 正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;

2. 反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。

反向代理一般在处理跨域请求的时候比较常用,属于服务端开发人员的日常操作,另外在缓存服务器、负载均衡服务器等等场景也是使用到代理模式的思想。


5. 代理模式的优缺点

代理模式的主要优点有:

1. 代理对象在访问者与目标对象之间可以起到 中介和保护目标对象 的作用;

2. 代理对象可以扩展目标对象的功能;

3. 代理模式能将访问者与目标对象分离,在一定程度上降低了系统的耦合度,如果我们希望适度扩展目标对象的一些功能,通过修改代理对象就可以了,符合开闭原则;

代理模式的缺点主要是增加了系统的复杂度,要斟酌当前场景是不是真的需要引入代理模式(十八线明星就别请经纪人了)。


6. 其他相关模式

很多其他的模式,比如状态模式、策略模式、访问者模式其实也是使用了代理模式,包括在之前高阶函数处介绍的备忘模式,本质上也是一种缓存代理。

6.1. 代理模式与适配器模式

代理模式和适配器模式都为另一个对象提供间接性的访问,他们的区别:

1. 适配器模式:主要用来解决接口之间不匹配的问题,通常是为所适配的对象提供一个不同的接口;

2. 代理模式:提供访问目标对象的间接访问,以及对目标对象功能的扩展,一般提供和目标对象一样的接口;

6.2. 代理模式与装饰者模式

装饰者模式实现上和代理模式类似,都是在访问目标对象之前或者之后执行一些逻辑,但是目的和功能不同:

1. 装饰者模式:目的是为了方便地给目标对象添加功能,也就是动态地添加功能;

2. 代理模式:主要目的是控制其他访问者对目标对象的访问;

推荐阅读:

1. Vue项目骨架屏注入实践

2. Proxy - Javascript | MDN

3. Object.defineProperty() - Javascript | MDN


推荐阅读
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • 开发笔记:Java是如何读取和写入浏览器Cookies的
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Java是如何读取和写入浏览器Cookies的相关的知识,希望对你有一定的参考价值。首先我 ... [详细]
  • 网络请求模块选择——axios框架的基本使用和封装
    本文介绍了选择网络请求模块axios的原因,以及axios框架的基本使用和封装方法。包括发送并发请求的演示,全局配置的设置,创建axios实例的方法,拦截器的使用,以及如何封装和请求响应劫持等内容。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • VueCLI多页分目录打包的步骤记录
    本文介绍了使用VueCLI进行多页分目录打包的步骤,包括页面目录结构、安装依赖、获取Vue CLI需要的多页对象等内容。同时还提供了自定义不同模块页面标题的方法。 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • 渗透测试基础bypass绕过阻挡我们的WAF(下)
    渗透测试基础-bypass ... [详细]
  • 找到JDK下载URL当然去官网找了。目前最新的1.8的下载URL(RPM)如下:http:download.oracle.comotn-pubjavajdk8u161-b122f3 ... [详细]
author-avatar
-断桥再见-_974_328
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有