JavaScript设计模式—代理模式

313 阅读4分钟

定义

一个对象提供一个代用品或占位符,以便控制对它的访问。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。

真实世界类比

代理模式

信用卡是银行账户的代理, 银行账户则是一大捆现金的代理。 它们都实现了同样的接口, 均可用于进行支付。 消费者会非常满意, 因为不必随身携带大量现金; 商店老板同样会十分高兴, 因为交易收入能以电子化的方式进入商店的银行账户中, 无需担心存款时出现现金丢失或被抢劫的情况。

模式实现-图片预加载(懒加载)

功能需求:用一张loading图占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里。

首先创建一个本体对象,负责往页面中创建 img 标签,并提供一个setSrc接口,用于外界设置 img 的src属性:

const myImage = (function() {
  const imgNode = document.createElement("img");
  document.body.appendChild(imgNode);
  return {
    setSrc: function(src) {
      imgNode.src = src;
    }
  };
})();

myImage.setSrc("http://qiniu.johnsenzhou.com/FmC3WzznEuwQwhEKn9YWn43phArJ");

现在开始引入代理对象proxyImage,在图片没加载出来前用loading图占位,提示用户图片正在加载。

const proxyImage = (function() {
  const img = new Image();
  img.onload = function() {
    myImage.setSrc(this.src);
  };
  return {
    setSrc: function(src) {
      myImage.setSrc(
        "http://www.sucaijishi.com/uploadfile/2015/0210/20150210104951657.gif"
      );
      img.src = src;
    }
  };
})();

proxyImage.setSrc("http://qiniu.johnsenzhou.com/FmC3WzznEuwQwhEKn9YWn43phArJ");

现在我们通过proxyImage间接访问myImageproxyImage控制了myImage对图片的直接操作,在此过程中加入一系列操作,然后再将处理好的请求转交给myImage

代理的意义

首先我们引入一个面向对象设计的原则——单一职责原则。

就一个类而言,应该只有一个引起它变化的原因。如果一个对象承担了多项职责,这个对象就会趋向臃肿,引起它变化的原因可能会有多个,也就等于把这些职责耦合在一起,这种耦合会导致脆弱和低内聚的设计。

然后再看之前写的程序,我们并没有改变或者增加myImage的接口,但是通过代理对象给它添加了新的行为。这符合开放-封闭原则。给img节点设置src和图片预加载这两个功能隔离在两个对象中,他们可以各自变化而不影响对方。如果后期不需要预加载了,只需要请求本体而不是请求代理对象即可。

代理和本体接口的一致性

上面说到如果后期不需要预加载功能是,只用改成直接请求本体即可。其中关键是代理和本体都具有setSrcd的接口。在客户看来,代理对象和本体对象是一致的。这样做有两个好处:

  • 用户可以放心的使用请求代理,而不需要弄清楚两者之间的区别。他只关系得到的结果是否一致。
  • 在任何使用本体的地方都可以放心的替换成使用代理。

模式实现-合并 HTTP 请求

在 web 开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步功能,当我们每选择一个CheckBox,他对应的文件就同步到另一台服务器上,如下图所示:

文件上传

首先我们先放置好checkbox节点:

<div>
  <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" />
</div>

然后确定代理对象处理逻辑:

收集 2 秒内的用户请求,等待 2 秒以后再把这 2 秒内需要同步的文件打包发给服务器。

const uploadFile = id => {
  console.log("开始上传文件,id为", id);
};

const proxyPploadFile = (function() {
  const cache = [];
  let timer;
  return function(id) {
    cache.push(id);
    if (timer) return; // 保证不覆盖已启动的定时器
    timer = setTimeout(() => {
      uploadFile(cache.join(","));
      clearTimeout(timer);
      timer = null;
      cache.length = 0;
    }, 2000);
  };
})();

const checkboxs = document.getElementsByTagName("input");
checkboxs.forEach(item => {
  item.onclick = function() {
    if (this.checked === true) {
      proxyPploadFile(this.id);
    }
  };
});

缓存代理

缓存代理可暂存一些开销大的运算结果,以便于下次运行同样的参数时直接返回结果。

计算乘积

const mult = function() {
  let result = 1;
  for (let i = 0; i < arguments.length; i++) {
    result = result * arguments[i];
  }
  return result;
};

const proxyMult = (function() {
  const cache = {};
  return function() {
    const args = Array.prototype.join.call(arguments, ",");
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
})();

proxyMult(1, 2, 3, 4); // 24
proxyMult(1, 2, 3, 4); // 24

缓存代理在优化列表翻页的时候经常可以用到:将之前选中的页码的数据缓存起来,后续再筛选到这个页码时直接从缓存中读取。

小结

代理模式非常有用,但是我们在编写业务代码时往往不需要去预测是否需要使用搭理模式,当真正发现不方便的时候再引入代理模式也不迟。