JS常见设计模式 之 代理模式

138 阅读9分钟

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

代理模式是一个十分多见的模式,我们甚至在日常生活中都可以找到很多的应用场景。比如明星的经纪人就是一个很常见的代理模式,通常情况下合作商要和明星合作,是不会和明星直接去进行商谈的,一般都是需要和经纪人去进行协商的,然后经纪人再将商谈好的合同拿给明星签字,经纪人对于明星来说就是一个代理中间人去协调合作商和明星双方事宜。

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

李大汉找老婆

村里家境一般的单身汉老李一直都想找一个老婆,以前中意的对象每次都因为自己口齿不清和说话笨笨的原因被女方所嫌弃了,所以这一次他打算去请隔壁家口齿伶俐的媒婆帮忙去说媒,他需要先把自己的优点长处告诉媒婆,然后让媒婆替自己去和翠花谈一谈,看看自己能不能和心仪许久的翠花成为一对儿。

我们用代码来分别实现一下两种场景。

不使用代理:

    // 老李的优点
    function Advantages() {
      return ['老实憨厚', '勤恳能干']
    }

    const laoli = {
      sayAdvantage(target) {
        const advantages = new Advantages()
        target.heard(advantages)
      }
    }

    const cuihua = {
      heard(advantages) {
        console.log('原来老李这么好啊,有这么多的优点:' + advantages)
      }
    }
    laoli.sayAdvantage(cuihua) // 原来老李这么好啊,有这么多的优点:老实憨厚,勤恳能干

接下来我们让媒婆给老李去说媒:

    // 老李的优点
    function Advantages() {
      return ['老实憨厚', '勤恳能干']
    }

    const laoli = {
      sayAdvantage(target) {
        const advantages = new Advantages()
        target.heard(advantages)
      }
    }

    const meipo = {
      heard(advantages) {
        cuihua.heard(advantages)
      }
    }

    const cuihua = {
      heard(advantages) {
        console.log('原来老李这么好啊,有这么多的优点:' + advantages)
      }
    }
    laoli.sayAdvantage(meipo) // 原来老李这么好啊,有这么多的优点:老实憨厚,勤恳能干

第二段代码是一个简单的代理模式的编写。

第二段代码和第一段代码的结果都是一样的,但是我们为什么要多此一举呢,反而使得逻辑更加的弯弯绕绕了。

现在我们加一种场景,媒婆比老李更会审时度势,知道什么时候说什么话,比如在翠花心情不错的时候谈婚论嫁要比在心情不好的时候成功率要高上很多,可是老李就不懂女生的这些小心思了啊,直来直去的一不小心就说错话了。代码如下:

    // 老李的优点
    function Advantages() {
      return ['老实憨厚', '勤恳能干']
    }

    const laoli = {
      sayAdvantage(target) {
        const advantages = new Advantages()
        target.heard(advantages)
      }
    }

    const meipo = {
      heard(advantages) {
        cuihua.listenGoodMood(function () { // 媒婆监听翠花的心情
          cuihua.heard(advantages)
        })
      }
    }

    const cuihua = {
      heard(advantages) {
        console.log('原来老李这么好啊,有这么多的优点:' + advantages)
      },
      listenGoodMood(fn) {
        setTimeout(() => { // 一秒后心情变好
          fn()
        }, 1000);
      }
    }
    laoli.sayAdvantage(meipo) // 原来老李这么好啊,有这么多的优点:老实憨厚,勤恳能干

上面的代码中媒婆就相当于是翠花的代理。说媒这件事的结果就是翠花不同意这门亲事,因为成熟男人最好的单品永远是自己的资产。

保护代理和虚拟代理

上面这个例子是一个虚拟代理的例子,但是我们可以换一种场景,就变成了保护代理。媒婆可以帮翠花去筛选掉一些不符合要求的请求者,类似老李这种一穷二白的老汉,这种请求可以直接在媒婆那里就被拒绝掉。这种代理就是一种保护代理。

另外,要把一个不是那么好的人形容成一个事业有成浑身自带光环的人,是非常耗费心神,同样的在代码里我们去new Advantages也是一个十分昂贵的操作,那么我们可以把new Advantages交给meipo去执行,meipo会在合适的时候(cuihua心情好的时候)再去new Advantages,这就是代理的另一种模式,被称为虚拟代理。

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

虚拟代理实现图片预加载

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

我们先来实现一个最基本的创建图片的功能:

     const createImg = (function () {
      const imgNode = document.createElement('img')
      document.body.appendChild(imgNode)
      return {
        setSrc(src) {
          imgNode.src = src
        }
      }
    })()
    createImg.setSrc('https://xxx.img')

上面的代码是一个自执行函数,我先创建了一个img标签,然后调用返回的对象里的setSrc方法来对img标签设置图片地址。

我们可以通过对浏览器设置网络来实现低速网络,可以看到图片加载出来之前是会有一段空白期的。

unProxyImg.gif 那么我们加入代理去实现加载前的占位图片

    const createImg = (function () {
      const imgNode = document.createElement('img')
      document.body.appendChild(imgNode)
      return {
        setSrc(src) {
          imgNode.src = src
        }
      }
    })()

    const proxyImg = (function() {
      const img = new Image;
      img.onload = function () {
        createImg.setSrc(this.src)
      }
      return {
        setSrc(src) {
          createImg.setSrc('./images/loading.jpeg')
          img.src = src
        }
      }
    })()
    
    proxyImg.setSrc('https://xxx.img')

上面的代码先通过一个虚拟的img标签去请求远程的要加载的图片并为真实需要展示的img标签设置一个站位图,等远程图片请求到浏览器之后再将该图片设置到需要展示的img标签上。

ProxyImg.gif

代理的意义

其实上面的图片预加载功能,即使不使用任何模式也能做到图片预加载啊,那么我们用代理模式去实现这个功能的意义在哪里,只是为了“炫技”吗?我们可以不用代理先实现一个简单的图片预加载功能:

    const unProxyPreloadImg = (function() {
      const imgNode = document.createElement('img')
      document.body.appendChild(imgNode)
      const img = new Image;
      img.onload = function () {
        imgNode.src = this.src
      }
      return {
        setSrc(src) {
          imgNode.src = './images/loading.jpeg'
          img.src = src
        }
      }
    })()
    unProxyPreloadImg.setSrc('https://xxx.img')

为了说明代理的意义,下面我们引入一个面向对象设计的原则——单一职责原则。

单一职责原则指的是,就一个类而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会早到意料之外的破坏。

上面一段代码中,unProxyPreloadImg对象除了要给img节点设置src之外还有负责预加载,如果以后我们这个方法的使用场景仅仅只是一个公司的内部的后台管理系统(网络良好),那么我们就要重新对这个代码进行拆解分离。

实际上我们根本要实现的功能的仅仅只是一个给img节点设置src,预加载只是一个锦上添花的可选功能。如果能把这个操作放在另一个对象里,自然是一个非常好的方法。代理模式的作用就提现在这里,代理负责预加载图片,预加载完成之后,把请求重新交给本体img节点。我们这样做就完全实现了两个功能的分离,将两个功能隔离在不同的对象之中,如果我们哪天不需要再用这个代理功能,我只需要改成请求本体即可。

虚拟代理合并HTTP请求

有这样一个场景,我们需要组一个局域网内的文件同步功能,即用户可以在PC端选取文件然后把文件通过局域网传输到其他设备上(其他PC或者mobile),文件列表中每个文件文件前面都有一个checkbox勾选框,当我们选中一个checkbox的时候,这个checkbox对应的文件会传到目标设备上。

我们先在页面上放入这些checkbox节点:

  <input type="checkbox" name="" id="file1">file1
  <input type="checkbox" name="" id="file2">file2
  <input type="checkbox" name="" id="file3">file3
  <input type="checkbox" name="" id="file4">file4
  <input type="checkbox" name="" id="file5">file5

接下来给这些checkbox绑定点击事件,并且在点击的时候把对应的文件同步到其他设备:

    function passFile(id) {
      console.log('开始同步文件,id为:' + id)
    }

    const checkboxes = document.getElementsByTagName('input')

    for (let i = 0; i < checkboxes.length; i++) {
      checkboxes[i].onclick = function () {
        if (this.checked === true) {
          passFile(this.id)
        }
      }
    }

当我们点一个checkbox的时候,往服务器发送一个请求,当我们连续点三次的时候,会连续往服务器发送是三个请求,如此频繁的网络请求将会带来相当大的开销。

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

    function passFiles(id) {
      console.log('开始同步文件,id为:' + id)
    }

    const proxyPassFiles = (function() {
      const cache = []
      let timer
      return function(id) {
        cache.push(id)
        if (timer) return

        timer = setTimeout(() => {
          passFiles(cache.join(','))
          clearTimeout(timer)
          timer = null
          cache.length = 0
        }, 2000)
      }
    })()

    const checkboxes = document.getElementsByTagName('input')

    for (let i = 0; i < checkboxes.length; i++) {
      checkboxes[i].onclick = function () {
        if (this.checked === true) {
          proxyPassFiles(this.id)
        }
      }
    }

惰性代理

当一个组件或者模块不是立马要被使用的时候,我们可以先不创建这个组件或者模块,等到它真正被使用的时候在去进行创建,但是我们又需要提前传一些参数进去,我们就可以用一个代理对象来存储这些数据,等到真正的对象被使用的时候再把代理对象里的数据设置到真正的组件或者模块中。

缓存代理

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

我们日常开发中最常用的就是深拷贝,深拷贝时需要一个缓存代理对象来存储之前已经存储过的对象key来防止循环引用。