JS设计模式之代理模式

定义:

为其他对象提供一种代理以控制对这个对象的访问。
何时使用:想在访问一个类时做一些控制。

如何解决:增加中间层。

应用实例:
1、明星使用经纪人做代理,请明星演出,只能联系他的经纪人,经纪人将所有演出细节和报酬谈妥之后,再将合同给明星签字。
2、买火车票不一定在火车站买,也可以去代售点。
如下图所示,就是在中间增加一个中间层做代理,例如es6的Proxy就是一个很好地例子

image.png

使用场景:
按职责来划分,通常有以下使用场景:
1、远程代理。
2、虚拟代理。
3、Copy-on-Write 代理。
4、保护(Protect or Access)代理。
5、缓存代理。
6、防火墙(Firewall)代理。
7、同步化(Synchronization)代理。
8、智能引用(Smart Reference)代理。

本文主要讲述的是js中常用的两种代理模式------> 虚拟代理和缓存代理

1、我们先来看如何使用虚拟代理实现图片的懒加载
//创建img对象,设置setSrc方法
let myImg = {
    setSrc(imgNode, src){
        imgNode.src = src;
    }
}
//创建代理对象,处理真正的逻辑
let ProxyImg = {
    setSrc(imgNode, src){
        myImg.setSrc(imgNode, 'file://image/loading.gif');
        let img = new Image();
        img.onload = () => {
            myImg.setSrc(imgNode, src);
        }
        img.src =  src;
    }
}

let imgNode = document.createElement('img');
imgSrc = 'https://upload.jianshu.io/users/upload_avatars/1413261/a0f9887a-
24f8-408c-bfd1-2975924e8f02.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/80/h/80/format/webp'
document.body.appendChild(imgNode);
ProxyImg.setSrc(imgNode, imgSrc);

上面的代码比较粗糙的解释了代理模式的作用,下面进行代码的优化

let myImg = (() =>{
    let imgNode = document.createElement('img');
    document.body.prepend(imgNode);
    return {
        setSrc(src){
            imgNode.src = src;
        }
    }
})()

let ProxyImg = ((src) => {
    let img = new Image();
    img.onload = () => {
        myImg.setSrc(img.src);
    }
    return {
        setSrc(src){
            myImg.setSrc('https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg');
            img.src =  src;
        }
    }
})()

imgSrc = 'https://upload.jianshu.io/users/upload_avatars/1413261/a0f9887a-
24f8-408c-bfd1-2975924e8f02.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/80/h/80/format/webp'
ProxyImg.setSrc(imgSrc);
案例小结

可以看出代码优化之后,结构更加清晰。
使用匿名函数自自执行,一方面可以不对外暴露接口与方法
另一方面使得创建dom节点与加载进行异步分离,某种程度上实现代码的解耦。

myImg对象负责创建dom节点,里面的setSrc负责渲染真正图片
代理对象proxyImg负责创建img对象,设置初始图片路径src,等图片onload方法执行之后,便开始将传入的src替换初始图片的src,从而实现图片占位的效果。

2、使用缓存代理实现计算结果的缓存
// 计算乘积
let mult = function(){
    let a = 1;
    for(let i = 0; i < arguments.length; i++){
        a = a * arguments[i];
    }
    console.log(a)
    return a;
}
// mult(1, 2);

// 创建缓存代理的工厂
let createProxyFactory = function(fn){
    let cache = {};
    return function(){
        let args = Array.prototype.join.call(arguments, ','); // '1,2,3,4'
        if(args in cache){
            return cache[args];
        }
        return cache[args] = fn.apply(this, arguments);
    }
}
let proxyMult = createProxyFactory(mult);
proxyMult(1, 2, 3, 4);
// 有了第一次cache对象中的缓存的结果,第二次直接返回
proxyMult(1, 2, 3, 4);

本次案例,第一次执行proxyMult的时候,返回一个function,当传入参数1234调用第一次的时候,缓存了一个计算记过{'1234', 24},当第二次传入同样的参数,便可以从cache对象中找到第一次的计算结果,直接返回即可,这样减少了计算。

案例小结:通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。现在这些方法被当做参数传入一个专门用于创建缓存代理的工厂中,这样一来,我们就可以为乘法、加法、减法等创建缓存代理。

大家可以思考一下缓存代理 如何用于ajax异步请求数据,同一页的数据理论上只需要去后台 领取一次,这些已经拉去到的数据在某个地方被缓存之后,下次再请求统同一页的时候,便可以直接使用之前的数据。

代理模式在实战中的应用 4.1 拦截器

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

  • 拦截器的思想在实战中应用非常多,比如我们在项目中经常使用 Axios 的实例来进行 HTTP 的请求,使用拦截器 interceptor 可以提前对 request 请求和 response 返回进行一些预处理,比如:
  • request 请求头的设置,和 Cookie 信息的设置;
  • 权限信息的预处理,常见的比如验权操作或者 Token 验证;
  • 数据格式的格式化,比如对组件绑定的 Date 类型的数据在请求前进行一些格式约定好的序列化操作;
  • 空字段的格式预处理,根据后端进行一些过滤操作;
  • response 的一些通用报错处理,比如使用 Message 控件抛出错误;除了 HTTP 相关的拦截器之外,还有路由跳转的拦截器,可以进行一些路由跳转的预处理等操作。

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

  • 现在的很多前端框架或者状态管理框架都使用上面介绍的 Object.definePropertyProxy 来实现数据的响应式化,比如 VueMobxAvalonJS 等,Vue 2.xAvalonJS 使用前者,而 Vue 3.xMobx 5.x 使用后者。
  • Vue 2.x 中通过 Object.defineProperty 来劫持各个属性的 setter/getter,在数据变动时,通过发布-订阅模式发布消息给订阅者,触发相应的监听回调,从而实现数据的响应式化,也就是数据到视图的双向绑定。

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

  • 无法监听利用索引直接设置数组的一个项,例如:vm.items[indexOfItem] = newValue;
  • 无法监听数组的长度的修改,例如:vm.items.length = newLength;
  • 无法监听 ES6SetWeakSetMapWeakMap 的变化;
  • 无法监听 Class 类型的数据;
  • 无法监听对象属性的新加或者删除;
  • 除此之外还有性能上的差异,基于这些原因,Vue 3.x 改用 Proxy 来实现数据监听了。当然缺点就是对 IE 用户的不友好,兼容性敏感的场景需要做一些取舍。

4.3 缓存代理

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

4.4 保护代理和虚拟代理

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

  • 保护代理 :当一个对象可能会收到大量请求时,可以设置保护代理,通过一些条件判断对请求进行过滤;
  • 虚拟代理 :在程序中可以能有一些代价昂贵的操作,此时可以设置虚拟代理,虚拟代理会在适合的时候才执行操作。

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

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

一个很大的图片加载前,一般使用菊花图、低质量图片等提前占位,优化图片加载导致白屏的情况; 现在很流行的页面加载前使用骨架屏来提前占位,很多 WebAppNativeApp 都采用这种方式来优化用户白屏体验

image

4.5 正向代理与反向代理

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

  • 正向代理: 一般的访问流程是客户端直接向目标服务器发送请求并获取内容,使用正向代理后,客户端改为向代理服务器发送请求,并指定目标服务器(原始服务器),然后由代理服务器和原始服务器通信,转交请求并获得的内容,再返回给客户端。正向代理隐藏了真实的客户端,为客户端收发请求,使真实客户端对服务器不可见;
  • 反向代理: 与一般访问流程相比,使用反向代理后,直接收到请求的服务器是代理服务器,然后将请求转发给内部网络上真正进行处理的服务器,得到的结果返回给客户端。反向代理隐藏了真实的服务器,为服务器收发请求,使真实服务器对客户端不可见。

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

image

5. 代理模式的优缺点

代理模式的主要优点有:

  • 代理对象在访问者与目标对象之间可以起到中介和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将访问者与目标对象分离,在一定程度上降低了系统的耦合度,如果我们希望适度扩展目标对象的一些功能,通过修改代理对象就可以了,符合开闭原则;

代理模式与适配器模式
代理模式和适配器模式都为另一个对象提供间接性的访问,他们的区别:
适配器模式: 主要用来解决接口之间不匹配的问题,通常是为所适配的对象提供一个不同的接口;
代理模式: 提供访问目标对象的间接访问,以及对目标对象功能的扩展,一般提供和目标对象一样的接口;

代理模式与装饰者模式
装饰者模式实现上和代理模式类似,都是在访问目标对象之前或者之后执行一些逻辑,但是目的和功能不同:
装饰者模式: 目的是为了方便地给目标对象添加功能,也就是动态地添加功能;
代理模式: 主要目的是控制其他访问者对目标对象的访问;
参考曾参的《JavaScript设计模式与开发实践》

你可能感兴趣的:(JS设计模式之代理模式)