代理模式
定义
当目标对象不方便直接访问或者访问者不满足要求的时候,提供一个代理对象来控制对目标对象的访问。访问者实际上访问的是代理对象。代理对象对请求做出一些处理之后,再把请求转交给目标对象。
主要解决
在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
优缺点
优点
- 职责清晰。
- 高扩展性。
- 智能化。
缺点
- 由于在客户端和真实主体之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
- 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
远程代理(主要针对Java)
远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。
Copy-on-Write 代理
定义
Copy-on-write (CoW or COW), sometimes referred to as implicit sharing or shadowing, is a resource-management technique used in computer programming to efficiently implement a “duplicate” or “copy” operation on modifiable resources.
写时复制(copy-on-write,简称 CoW 或 COW),也叫隐式共享(implicit sharing)或隐藏(shadowing),是计算机编程中的一种资源管理技术,用于高效地复制或拷贝可修改资源
If a resource is duplicated but not modified, it is not necessary to create a new resource; the resource can be shared between the copy and the original. Modifications must still create a copy, hence the technique: the copy operation is deferred to the first write. By sharing resources in this way, it is possible to significantly reduce the resource consumption of unmodified copies, while adding a small overhead to resource-modifying operations.
具体的,如果复制了一个资源但没有改动,就没必要创建这个新的资源,此时副本能够与原版共享同一资源,在修改时仍需要创建副本。因此,关键在于:将拷贝操作推迟到第一次写入的时候。通过这种方式来共享资源,能够显著减少无改动副本的资源消耗,而只是略微增加了资源修改操作的开销
实例
但是在JavaScript中,以前还不好对这种代理进行很好的描述,因为以前JavaScript并没有对对象属性进行监听的方法(实际上也只有obj.defineProperty
),在ES6引入了Proxy
之后,我们可以很方便的实现COW
代理。下面给出简单实现:
1 | let person = { |
上面的例子中,充分利用了JavaScript弱类型语言的特征,在最开始时将p_person
赋值为Proxy
对象,在执行get操作时,直接返回被访问对象的值;在执行set操作时,在对被访问对象执行深拷贝,并把p_person
赋值为新对象。
但是这样存在一些问题:
Proxy
对象同样会占用空间,如果确定目标对象大概率会被改动,并且目标对象并不是很大,建议直接deep_clone
。Proxy
对象内部的定义比较复杂,每一个对象都要重复该过程。所以我们封装一个函数,来执行这个过程。
1 | let person = { |
这样我们就解决了第二个问题,第一个问题是无法解决的,需要我们自行解决。
保护(Protect or Access)代理
定义
保护代理模式(Access Proxy), 也叫Protect Proxy. 这种代理用于对真实对象的功能做一些访问限制, 在代理层做身份验证. 通过了验证, 才调用真实的主体对象的相应方法。
这种限制,应该分为两种:
- 对访问来源做限制
- 对访问内容做限制
实例
但是JavaScript中目前不能对一种进行限制(使用透明代理),我们使用Proxy
API来实现对访问内容进行限制:
1 | //加入我们要对sex进行读限制,对person进行写限制 |
同样的,我们也可以对content_p
进行封装,创建一个工厂,以进行更好复用代码。
如果实际要对访问来源做限制,只能使用非透明的方式,即读写必须通过函数来进行操作。
1 | let content = { |
缓存(Cache)代理
定义
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算的时候,如果传递进来的参数与之前的一致,则直接返回结果,减少运算。
实例
下面是一个对乘积进行缓存的代理:
1 | let mult = function{ |
同样的,我们可以对proxy_mult
进行封装,创建一个工厂,来生成函数工厂。
1 | //计算乘积函数 |
虚拟代理
定义
虚拟代理作为创建开销大的对象的代表,经常会直到我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
实例
合并请求减少开销
这个用法其实和函数的节流效果一致,将多次操作合并为一次,以减少消耗,特别是网络请求。
比如文件同步时,虽然我们和设计checkbox
来合并请求,但是并不是每个用户都会按我们设计的逻辑来进行操作。他们可能仍然一个一个地同步,这样会加大服务器的负担。所以我们可以把一段时间内的请求存储下来,每隔一段时间进行同步。以减少服务器的负担。
1 | let syncFile = function(id){ |
惰性加载
这个作用就是虚拟代理最初的用法。
比如我们有一个库名叫composite
,其作用是
- 查询页面内图片的的OCR文字(picOcr)
- 建立页面的虚拟DOM(virtualDOM)
- 进行大数计算(bigNumberCalc)
- 用webGL绘制一个游戏(webGLGame)
可以想象,这个库的每个功能都是比较困难,所以其代码量都比较大,并且我们并不一定会使用到其所有功能,假如picOcr
是最常用的模块,我们直接加载该模块。所以我们可以将其他的模块做成懒加载,需要使用时再加载。
1 | const composite = { |
当然,上面的设计是最简单的模块设计方式,有很多问题,但是这不是我们讨论的重点,我们这里讨论的重点是延迟加载部分,可以看到
virtualDOM
模块最初是没有被加载的,在调用的时候我们才通过网络加载,最后覆盖原始对象,下次调用时就是直接调用实际代码了。
但实际上这不是好的方案,好的延迟加载方案是:
在库被加载完成后,通过异步网络同时加载需要的模块,这样用户在调用其他模块时,也不会感觉到延迟,同时不会阻塞页面。
其他代理
在代理模式中,还有其他的模式,但是在JavaScript中并不是常用的,比如:
防火墙代理
控制网络资源的访问,保护服务器资源的安全。
远程代理
为一个对象在不同的地址空间提供局部代表。在JavaScript中,远程代理可以是另一个虚拟机中的对象。
智能引用代理
取代了简单的指针,它在访问对象的同时执行一些附加操作,比如计算一个对象被引用的次数。
ES6的Proxy
在ES6之前,JavaScript只能通过Object.defineProperty
来进行数据代理。并且还无法做到对对象属性的代理监听。以至于Vue不得不设计一个Vue.prototype.set
来监听对象属性的修改。但是ES6的的Proxy
API在编程语言的层面上提供了代理,这样使得我们这一章的代理模式的实现变得极其简单。下面简单介绍其用法。
ES6提供原生的Proxy
构造函数,用来生成Proxy
实例。
1 | let proxy = new Proxy(target, handler) |
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,-
target
参数表示所要拦截的目标对象。handler
参数也是一个对象,用来定制拦截行为。
如果handler
没有做任何拦截为{}
,则直接通向原对象。
示例
1 | let stu = { |
上面代理拦截了get
和set
方法,拦截了获取stu
对象的sex
属性、设置stu
的name
属性。
其中handler中,支持的拦截属性有13种:
- **get(target, propKey, receiver)**:拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - **has(target, propKey)**:拦截
propKey in proxy
的操作,返回一个布尔值。 - **deleteProperty(target, propKey)**:拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - **ownKeys(target)**:拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - **getOwnPropertyDescriptor(target, propKey)**:拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - **defineProperty(target, propKey, propDesc)**:拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - **preventExtensions(target)**:拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - **getPrototypeOf(target)**:拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - **isExtensible(target)**:拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - **setPrototypeOf(target, proto)**:拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
简单介绍上面的参数分别为:
target:目标对象,即被代理的对象
propKey: 操作的属性键名
value:操作的属性值
receiver:代理对象
propKey:属性键名
propDesc:属性的描述,详见Object.defineProperty()
proto:原型对象
Vue3中的响应式完全依赖了这个API,所以其是非常有用的。用起来也简单。还是需要深入理解。