js设计模式之代理模式

代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。javascript

在某些状况下,直接访问对象不方便或者对访问对象加强一些功能,可使用到代理模式。好比想请一个明星来办一场商业演出,通常都是联系明星的经纪人,那么经纪人就是明星的代理。前端

1.小明追妹子的故事

在这个故事中,假设妹子是girl对象,小明想要给妹子送花。因为妹子只有一个,就直接经过一个对象字面量表示。java

class Gift {}

class Person {
  constructor(name, job) {
    this.name = name;
    this.job = job;
  }

  sendGift(target) {
    const gift = new Gift();
    target.receiveGift(this, gift);
  }
}

const girl =  {
  receiveGift(sender, gift) {
    console.log(`from ${sender.name}`, sender, gift);
  }
}

const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(girl); // from 小明 Person {name: "小明", job: "程序员"} Gift {}
复制代码

如今妹子收到礼物了,也知道了小明的姓名和工做。但是追求妹子的人不少,妹子一我的收不过来啊,这时候妹子就须要一个代理对象了,称为proxyGirl。react

class Gift {}

class Person {
  constructor(name, job) {
    this.name = name;
    this.job = job;
  }

  sendGift(target) {
    const gift = new Gift();
    target.receiveGift(this, gift);
  }
}

const proxyGirl = {
  receiveGift(...args) {
    girl.receiveGift(...args);
  }
}

const girl =  {
  receiveGift(sender, gift) {
    console.log(`from ${sender.name}`, sender, gift);
  }
}

const xiaoming = new Person('小明', '程序员');
xiaoming.sendGift(proxyGirl);
复制代码

这里结果和上述同样,所作的就是增长了一个代理对象。这必然会增长一些代码,增长程序的复杂度。它的好处在于能够经过代理对象,去控制对目标对象的直接访问(见定义)。程序员

好比在proxyGirl中去进行一些过滤。ajax

const proxyGirl = {
  receiveGift(...args) {
    const sender = args[0];
    if(sender.job !== '程序员') {
        girl.receiveGift(...args);
    } else {
        throw sender;
    }
  }
}
复制代码

若是给妹子送礼物的是程序员,那么把他扔出去。缓存

2.保护代理和虚拟代理

从上述例子中,能够看到两种代理方式的影子。代理对象能够帮目标对象过滤掉一些请求,好比职业是程序员的,或者没房没车的。这种代理叫作保护代理bash

另外,假设礼物价值不菲,在程序中new Gift也是一个代价昂贵的操做。那么咱们能够把这个操做交给代理类去执行。代理类首先过滤掉不符合条件的人,而后去new Gift,这是代理类的另外一种形式,叫作虚拟代理,也叫作动态代理。虚拟代理把一些开销很大的对象,延迟到真正须要它的时候才去建立(相似于单例模式中的惰性单例)。服务器

const proxyGirl = {
  receiveGift(sender) {
    const sender = args[0];
    if(sender.job !== '程序员') {
        const gift = new Gift();
        girl.receiveGift(sender, gift); // 不改变目标对象的参数
    } else {
        throw sender;
    }
  }
}
复制代码

3.虚拟代理实现图片预加载

前端开发中,直接给img设置目标src不是一个好的作法。当图片体积比较大的时候,不能第一时间显示出来,就会形成空白,这很显然不是一个好的体验。常见的作法是给图片预先设置一个loading图(或分辨率较低的原图),而后用异步的方式加载图片,加载好后再替换原图片的url。这种场景就很适合时候虚拟代理(给目标对象增长loading功能)。网络

const myImage = {
  setSrc: (ele, src) => {
    ele.src = src;
  }
}


const proxyImage = {
  checkEle: ele => {
    if(ele.tagName !== 'IMG') {
      throw '这个对象只能代理img标签';
    }
  },

  setSrc: (ele, src) => {
    // 初始设置为loading图片
    this.checkEle();
    // 设置loading
    ele.src = 'loading.png';
    // 图片下载好了以后替换原图的url
    const img = new Image();
    img.src = src;
    img.onload = () => {
      myImage.setSrc(ele, src);
    }
  }
}

const img = document.querySelector('.some-img');
proxyImage.setSrc(img, 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1547377781665&di=6f7dd28462a295f04da213e190728681&imgtype=0&src=http%3A%2F%2Fb.zol-img.com.cn%2Fdesk%2Fbizhi%2Fstart%2F2%2F1363857521405.jpg')

复制代码

咱们经过代理对象proxyImage间接访问目标对象myImage,并添加过滤标签功能,增长loading功能。

4.代理的意义

上面的实现咱们彻底能够放在myImage对象中。

const myImage = {
  setSrc: (ele, src) => {
    if(ele.tagName !== 'IMG') {
      throw '这个对象只能代理img标签';
    }
    
    // 设置loading
    ele.src = 'loading.png';
    // 图片下载好了以后替换原图的url
    const img = new Image();
    img.src = src;
    img.onload = () => {
      ele.src = src;
    }
  }
}
复制代码

好像也没有什么问题。代码确实能正常工做,并达到了预期的效果。不过它违反了单一职责原则。职责被定义为“引发变化的缘由”,就是说有且只有一个缘由引发对象的变化。若是多个缘由都能引发对象变化,那么说明这个对象承担了过多的职责,它将变得巨大,而且职责之间相互耦合,那么必将致使高耦合低内聚的设计。咱们在处理其中一个职责时,有可能由于强耦合性影响到另外一个职责的实现。这对于测试来讲也是很是不便的。

另外,在面向对象的设计中,大多数状况下,若是违反其余任何原则,同时将违背开放封闭原则。将来,若是网速很是快,再也不须要loading了,那么咱们要移除loading,就必须修改myImage对象。

实际上,myImage对象中,只须要实现给img标签添加src的功能。loading功能和过滤功能只是锦上添花。若是能把这些加强功能放在另外一个对象里面,天然是极好的设计。因而代理的做用在这里就体现出来了。代理加强过滤标签和loading功能,操做完成后,把请求从新交给本体myImage。

5.代理和本体接口的一致性

代理对象和本体对象的接口(参数)应该保持一致。 上述例子中,若是不须要加强功能的时候,咱们彻底可使用myImage对象替换proxyImage对象。在客户看来,代理对象和本体是一致的,客户并不须要知道代理和本体的区别,这样有两个好处。

  • 用户能够放心请求代理,它只关心是否获得想要的结果。
  • 在任何使用本体的地方均可以使用代理。

第二点让我想到了里氏替换原则。

里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类能够出现的地方,子类必定能够出现。

代理类能够看作是继承了目标类,并对其进行了加强。

此外,上面一直在谈论代理对象。注意:函数也是一个对象。

const myImage = (ele, src) => ele.src = src;
const proxyImage = (ele, src) => {
    // loading功能,省略
    myImage(ele, src);
}
复制代码

6.虚拟代理合并http请求

若是页面上有n多个checkbox,点击一个checkbox都要发送一个请求,请求携带checkbox的uniqueId参数。频繁的网络请求会带给服务器压力。最初的代码是这样的:

const postRequest = id => {
  // 发送请求操做,忽略
}

const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
  checkbox[i].onClick = function() {
    postMessage(this.unique_id);
  }
}
复制代码

那么怎样经过虚拟代理合并呢。

const postRequest = id => {
  // 发送请求操做,忽略
}

const proxyPostRequest = (() => {
  const caches = [];
  let timer;
  return id => {
    caches.push(id);
    if(timer) {
      return;
    }
    timer = setTimeout(() => {
      postRequest(caches.join(','));
      caches.length = 0;
      timer = null;
    }, 2000);
  }
})()

const checkbox = document.querySelectorAll('input[type="checkbox"]');
for(let i = 0; i < checkbox.length; i++) {
  checkbox[i].onClick = function() {
    proxyPostRequest(this.unique_id);
  }
}
复制代码

proxyPostRequest是一个IIFE,返回一个闭包。请求不要同时发出,而是两秒后合并id,只发送一次。

proxyPostRequest应用了函数柯里化(function currying)的思想。

currying又称为部分求值。一个currying的函数首先会接受一些参数,接受了这些参数以后,并不会当即求值,而是继续返回另一个函数,刚才传入的参数在函数造成的闭包中被保存起来。待到函数被真正须要求值的时候,以前传入的全部参数都会被一次性用于求值。

7.缓存代理

缓存代理能够为一些开销大的运算结果提供暂时的存储。在下次计算时,若是传递进来的参数跟以前一致,则能够直接返回以前缓存的结果。这须要不含反作用的函数(若是函数中有Date.now()、Math.random()、外部变量等参与了计算,那么可能会致使缓存的结果并不正确)。

1.缓存计算结果

// 假设这里的add有巨大的计算量(狗头)
var add = (...args) => {
  return args.reduce((prev, curr) => {
    return prev += curr;
  }, 0)
}

const proxyAdd = (() => {
  const caches = [];
  return (...args) => {
    let value = caches[args.join(',')];
    if(value !== undefined) {
      return value;
    }

    return caches[args.join(',')] = add(...args);
  }
})()

proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3);
proxyAdd(1, 2, 3, 4);
复制代码

2.缓存ajax请求

实际开发中,如某些展现性的表格,分页的数据不须要重复拉取。拉取一次后,换缓存下来,下次使用能够直接访问了。react开发中能够避免重复调动action。

// action/xxx.js
const fetchPageData = (id) => (() => {
  const caches = [];
  return dispatch => {
    if(caches[id] !== undefined) {
      return;
    }

    var data = fetchxxx(id);
    if(data) {
      caches[id] = id;
      dispatch(storeData({
        type: xxx,
        data,
      }))
    }
    return data;
  }
})()
复制代码

显然这里可使用缓存代理达到请求。

8.用高阶函数动态建立代理

上述缓存加速结果例子中,只能缓存加法的结果。若是须要缓存乘法的结果,那么又要建立一个proxyMulti的函数。这会写重复代码。可使用工厂模式来建立缓存代理。

return args.reduce((prev, curr) => {
    return prev += curr;
  }, 0)
}

const multi = (...args) => {
  return args.reduce((prev, curr) => {
    return prev *= curr;
  }, 1)
}

const createProxyFactory = fn => {
  const caches = [];
  return (...args) => {
    let value = caches[args.join(',')];
    if(value !== undefined) {
      return value;
    }
    return caches[args.join(',')] = fn.apply(this, args);
  }
}

const proxyAdd = createProxyFactory(add);
proxyAdd(1, 2, 3, 4);

const proxyMulti = createProxyFactory(multi);
proxyMulti(1, 2, 3, 4);

复制代码

9.其余代理模式

代理模式的变种很是多,限于篇幅以及在js的适用性,一下代理简单介绍一下。

  • 防火墙代理:控制网络资源的访问,保护主机不让“坏人”靠近。
  • 远程代理:为一个对象在不一样的地址空间提供局部列表,在java中,远程代理能够是另外一个虚拟机的对象。
  • 保护代理:用户对象应该有不一样访问权限的状况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操做,好比计算引用对象呗引用的次数(怎么让我想到了getter setter)。
  • 写时复制代理:一般用于复制一个庞大对象的状况。写时复制代理延迟了复制的过程。当对象被真正修改时,才对它进行复制操做。写时复制代理是虚拟代理的一种变体,dll是其典型运用场景。

10.小结

代理模式的定义是:为一个对象提供代理,来控制对这个对象的访问。

优势:

  1. 经过代理目标类,让目标类职责清晰。
  2. 代理类具备高扩展性。
  3. 智能化--缓存代理。

缺点:

  1. 因为在客户和真实对象之间增长了代理对象,所以有些类型的代理模式可能会形成请求的处理速度变慢。
  2. 实现代理模式须要额外的工做,有些代理模式的实现很是复杂。

和其余模式的区别

一、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

二、和装饰器模式的区别:装饰器模式为了加强功能,而代理模式是为了加以控制(文中给图片鞥家loading的时候,彷佛区分不是那么明显)。

代理模式分类庞杂,在JS中最经常使用的是保护代理、虚拟代理和缓存代理(文中都用到了)。虽然代理模式很是有用,但不须要预先猜想是否须要使用代理,当发现不方便直接访问某个对象的时候,再编写代理也不迟。

相关文章
相关标签/搜索