纵有疾风起
人生不言弃

代理模式

摘自《JavaScript设计模式与开发实践》

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。

小明追 MM 的故事

在四月一个晴朗的早晨,小明遇见了他的百分百女孩,我们暂且称呼小明的女神为 A。两天之后,小明决定给 A 送一束花来表白。刚好小明打听到 A 和他有一个共同的朋友 B,于是内向的小明决定让 B 来代替自己完成送花这件事情。

const Flower = function () {  }  const xiaoming = {    sendFlower: function (target) {      const flower = new Flower()      target.receiveFlower(flower)    }  }  const B = {    receiveFlower: function (flower) {      A.receiveFlower(flower)    }  }  const A = {    receiveFlower: function (flower) {      console.log('收到花 ' + flower)    }  }  xiaoming.sendFlower(B)

小明跟 A 刚刚认识两天,还无法辨别 A 什么时候心情好。如果不合时宜地把花送给 A,花被直接扔掉的可能性很大。但是 A 的朋友 B 却很了解 A,所以小明只管把花交给 B,B 会监听 A 的心情变化,然后选择 A心情好的时候把花转交给 A,代码如下:

const Flower = function () {  }  const xiaoming = {    sendFlower: function (target) {      const flower = new Flower()      target.receiveFlower(flower)    }  }  const B = {    receiveFlower: function (flower) {      A.listenGoodMood(function () { // 监听 A 的好心情        A.receiveFlower(flower)      })    }  }  const A = {    receiveFlower: function (flower) {      console.log('收到花 ' + flower)    },    listenGoodMood: function (fn) {      setTimeout(function () { // 假设 10 秒之后 A 的心情变好        fn()      }, 10000)    }  }  xiaoming.sendFlower(B)

保护代理和虚拟代理

假设现实中的花价格不菲,导致在程序世界里, new Flower 也是一个代价昂贵的操作,那么我们可以把 new Flower 的操作交给代理 B 去执行,代理 B 会选择在 A 心情好时再执行 new Flower ,这是代理模式的另一种形式,叫作虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下:

const B = {    receiveFlower: function (flower) {      A.listenGoodMood(function () { // 监听 A 的好心情        const flower = new Flower()        A.receiveFlower(flower)      })    }  }

保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是最常用的一种代理模式。

虚拟代理实现图片预加载

在 Web开发中,图片预加载是一种常用的技术,如果直接给某个 img 标签节点设置 src 属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。

  const myImage = (function() {    const imgNode = document.createElement('img')    document.body.appendChild(imgNode)    return {      setSrc: function( src ){        imgNode.src = src      }    }  })()  myImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' )

我们把网速调至 5KB/s,然后通过 MyImage.setSrc 给该 img 节点设置 src ,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。

现在开始引入代理对象 proxyImage ,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位图 loading.gif, 来提示用户图片正在加载。代码如下:

  const myImage = (function () {    const imgNode = document.createElement('img')    console.log(imgNode)    document.body.appendChild(imgNode)    return {      setSrc: function (src) {        imgNode.src = src      }    }  })()  const proxyImage = (function () {    const img = new Image    img.onload = function () {      console.log(this)      myImage.setSrc(this.src)    }    return {      setSrc: function (src) {        myImage.setSrc('./loading.gif')        img.src = src        console.log(img)      }    }  })()  proxyImage.setSrc('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1109917053,4211270766&fm=27&gp=0.jpg')

虚拟代理合并 HTTP 请求

在 Web 开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox 的时候,它对应的文件就会被同步到另外一台备用服务器上面。

<body><input type="checkbox" id="1"><input type="checkbox" id="2"><input type="checkbox" id="3"><input type="checkbox" id="4"><input type="checkbox" id="5"><input type="checkbox" id="6"><input type="checkbox" id="7"><input type="checkbox" id="8"><input type="checkbox" id="9"></body><script>  // 接下来,给这些 checkbox绑定点击事件,并且在点击的同时往另一台服务器同步文件:  const synchronousFile = function (id) {    console.log('开始同步文件,id 为: ' + id)  }  const checkbox = document.getElementsByName('input')  for (let i = 0, c; c = checkbox[i++];) {    c.onclick = function() {      if(this.checked === true) {        synchronousFile(this.id)      }    }  }</script>

当我们选中 3 个 checkbox 的时候,依次往服务器发送了 3 次同步文件的请求。而点击一个 checkbox 并不是很复杂的操作,作为 APM250+的资深 Dota玩家,我有把握一秒钟之内点中 4个 checkbox。可以预见,如此频繁的网络请求将会带来相当大的开销。

解决方案是,我们可以通过一个代理函数proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待 2 秒之后才把这 2 秒之内需要同步的文件 ID 打包发给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。代码如下:

const synchronousFile = function (id) {    console.log('开始同步文件,id 为: ' + id)  }  const proxySynchronousFile = (function () {    let cache = [] // 保存一段时间内需要同步的 ID    let timer    return function (id) {      cache.push(id)      if (timer) { // 保证不会覆盖已经启动的定时器        return      }      timer = setTimeout(function () {        synchronousFile(cache.join(',')) // 2 秒后向本体发送需要同步的 ID 集合        clearTimeout(timer) // 清空定时器        timer = null        cache.length = 0 // 清空 ID 集合      }, 2000)    }  })()  const checkbox = document.getElementsByName('input')  for (let i = 0, c; c = checkbox[i++];) {    c.onclick = function () {      if (this.checked === true) {        synchronousFile(this.id)      }    }  }

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

缓存代理的例子——计算乘积

为了节省示例代码,以及让读者把注意力集中在代理模式上面,这里编写一个简单的求乘积的程序,请读者自行把它脑补为复杂的计算。

  // 先创建一个用于求乘积的函数:  const mult = function () {    let a = 1    for (let i = 0, l = arguments.length; i < l; i++) {      a = a * arguments[i]    }    return a  }  // 现在加入缓存代理函数:  const proxyMult = (function () {    let cache = {}    return function () {      const args = Array.prototype.join.call(arguments, ',')      console.log(typeof args)      if (args in cache) { // 计算结果如果保存在缓存对象中,直接从缓存中取值        return cache[args]      }      return cache[args] = mult.apply(this, arguments)    }  })()  proxyMult(1, 2, 3, 4)
用高阶函数动态创建代理

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

  /**************** 计算乘积 *****************/  const mult = function () {    let a = 1    for (let i = 0, l = arguments.length; i < l; i++) {      a = a * arguments[i]    }    return a  }  /**************** 计算加和 *****************/  const plus = function () {    let a = 0    for (let i = 0, l = arguments.length; i < l; i++) {      a = a + arguments[i]    }    return a  }  /**************** 创建缓存代理的工厂 *****************/  const createProxyFactory = function (fn) {    const cache = {}    return function () {      const args = Array.prototype.join.call(arguments, ',')      if (args in cache) {        return cache[args]      }      return cache[args] = fn.apply(this, arguments)    }  }  const proxyMult = createProxyFactory(mult),    proxyPlus = createProxyFactory(plus)  alert(proxyMult(1, 2, 3, 4)) // 输出:24  alert(proxyPlus(1, 2, 3, 4)) // 输出:10  alert(proxyPlus(1, 2, 3, 4)) // 输出:10

其他代理模式

  • 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另一个虚拟机中的对象。
  • 保护代理:用于对象应该有不同访问权限的情况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。

文章转载于:https://www.jianshu.com/p/823b1b46cb09

原著是一个有趣的人,若有侵权,请通知删除

未经允许不得转载:起风网 » 代理模式
分享到: 生成海报

评论 抢沙发

评论前必须登录!

立即登录