前端设计模式 - 代理模式/中介模式

367 阅读5分钟

javaScript代理模式

理解

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

生活中的代理模式:比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。

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

image.png

代理的意义

  • 保证单一职责原则;

    本体不用负担太多职责,比如下面的例子图片预加载中本体只是为 img 节点设置 src ,预加载图片只是一个锦上添花的功能。如果能把这个操作放在另一个对象里面,自然是一个非常好的方法。

  • 保证开放—封闭原则

    不会改变本体,但是可以通过代理为本体添加新功能。例如:给 img 节点设置 src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。

  • 代理和本体接口的一致性

    在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。

    • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
    • 在任何使用本体的地方都可以替换成使用代理。

场景一:虚拟代理实现图片预加载

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

直接设置src:

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);

  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();

myImage.setSrc("http://...");

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

var myImage = (function () {
  var imgNode = document.createElement("img");
  document.body.appendChild(imgNode);

  return {
    setSrc: function (src) {
      imgNode.src = src;
    },
  };
})();

var proxyImage = (function () {
  var img = new Image();
  img.onload = function () {
    myImage.setSrc(this.src);
  };

  return {
    setSrc: function (src) {
      myImage.setSrc("file://..../loading.gif");
      img.src = src;
    },
  };
})();

proxyImage.setSrc("http://...");

proxyImage 控制了客户对 MyImage 的访问,并且在此过程中加入一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置为一张本地的 loading 图片。

场景二:虚拟代理合并HTTP请求

假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同步到另外一台备用服务器上面:

var synchronouaFile = function (id) {
  console.log("开始同步文件,id为:" + id);
};

var checkbox = document.getElementsByTagName("input");
for (var i = 0, c; (c = checkbox[i++]); ) {
  c.onclick = function () {
    if (this.checked === true) {
      synchronouaFile(this.id);
    }
  };
}

当我们选中3个 checkbox 的时候,依次往服务器发送了3次同步文件的请求,这显然是会导致产生的请求的。

解决方案是,我们可以通过一个代理函数 proxySynchronousFile 来收集一段时间之内的请求,最后一次性发送给服务器。

var synchronousFile = function (id) {
  console.log("开始同步文件,id为:" + id);
};

var proxySynchronousFile = (function () {
  var cache = [], // 保存一段时间内需要同步的ID
    timer; // 定时器
  return function (id) {
    cache.push(id);
    if (timer) {
      // 保证不会覆盖已经启动的定时器
      return;
    }

    timer = setTimeout(function () {
      synchronousFile(cache.join(",")); // 两秒后向本体发送需要同步的ID集合
      clearTimeout(timer);
      timer = null;
      cache.length = 0; // 清空ID集合
    }, 2000);
  };
})();

var checkbox = document.getElementsByTagName("input");
for (var i = 0, c; (c = checkbox[i++]); ) {
  c.onclick = function () {
    if (this.checked === true) {
      proxySynchronousFilea(this.id);
    }
  };
}

场景三:缓存代理

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

计算乘积

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

mult(2, 3); //6
mult(2, 3, 4); //24

加入代理缓存:

var proxyMult = (function () {
  var cache = {};
  return function () {
    var 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

当我们第二次调用 proxyMult( 1, 2, 3, 4 ) 的时候,本体 mult 函数并没有被计算,proxyMult 直接返回了之前缓存好的计算结果。

ajax异步请求数据

我们在常常在项目中遇到分页的需求,同一页的数据理论上只需要去后台拉取一次,这些已经拉取到的数据在某个地方被缓存之后,下次再请求同一页的时候,便可以直接使用之前的数据。

显然这里也可以引入缓存代理,实现方式跟计算乘积的例子差不多,唯一不同的是,请求数据是个异步的操作,我们无法直接把计算结果放到代理对象的缓存中,而是要通过回调的方式。

用高阶函数动态创建代理

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。

// 计算乘积
var mult = function(){
    var a = 1;
    for(var i = 0; i < arguments.length; i++){
        a = a * arguments[i];
    }
    return a;
}
//计算加和
var plus = function(){
  var a = 0;
  for(var i = 0; i < arguments.length; i++){
    a = a + arguments[i];
  }
  return a;
}
//创建缓存代理的工厂
var createProxyFactory = function(fn){
  var cache = {};
  return function(){
  	var args = Array.prototype.join.call(arguments,',');
    if(args in cache){
    	return cache[args];
    }
    return cache[args] = fn.apply(this,arguments);
  }
}

var proxyMult = createProxyFactory(mult),
    proxyPlus = createProxyFactory(plus);

console.log(proxyMult(1,2,3,4));//24
console.log(proxyMult(1,2,3,4));//24
console.log(proxyPlus(1,2,3,4));//10
console.log(proxyPlus(1,2,3,4));//10

小结

代理模式包括许多小分类,在 JavaScript 开发中最常用的是虚拟代理和缓存代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。

应用中的代理模式

此标题我把它称为应用中的代理,是因为学习框架源码,或者遇到工作需求时,我们会知道会需要知道的这几种方法。

defineProperty

Object.defineProperty() 直接在对象上定义新属性,或修改对象上现有的属性,并返回该对象。 例子:

const person = {
  name:"Hearling",
  age:18,
  nationality:"China"
};

Object.defineProperty(person, 'property1', {
  value: 42,
  writable: false
});

person.property1 = 77;
// throws an error in strict mode

console.log(person.property1);
// expected output: 42

例子中使用defineProperty中的可选配置writable 配置是否可以使用赋值运算符更改与属性关联的值, 除此有configurable、enumerable、value、get、set 等可配置参数

另外的例子:

function person() {
}

var value;
Object.defineProperty(person.prototype, "x", {
  get() {
    return value;
  },
  set(x) {
    value = x;
  }
});

var a = new person();
var b = new person();
a.x = 1;
console.log(b.x); // 1

Proxy

使用 Proxy 对象,我们可以更好地控制与某些对象的交互。代理对象可以在我们与对象交互时确定行为,例如当我们获取值或设置值时。

例子:

const person = {
  name:"Hearling",
  age:18,
  nationality:"China"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${obj[prop]}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    obj[prop] = value;
  }
});

我们希望与代理对象进行交互,而不是直接与此对象交互。在 JavaScript 中,我们可以通过创建 Proxy 的新实例轻松地创建新代理。

Proxy 的第二个参数是一个表示处理程序的对象。在处理程序对象中,我们可以根据交互的类型定义具体的行为。虽然有很多方法可以添加到 Proxy 处理程序,但最常见的两个是 getset

  • get:尝试访问属性时调用,我们希望代理记录一个更易读的句子,其中包含属性的键和值。
  • set:尝试修改属性时调用,我们希望代理记录属性的先前值和新值。

Reflect

JavaScript 提供了一个名为 Reflect 的内置对象,它使我们在使用代理时更容易操作目标对象。

我们可以通过 Reflect.get()Reflect.set() 访问或修改目标对象上的属性,而不是通过 obj[prop] 访问属性或通过 obj[prop] = value 设置属性。这些方法接收与处理程序对象上的方法相同的参数。

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    return Reflect.set(obj, prop, value);
  },
});

personProxy.name; // The value of name is Hearling
personProxy.age = 43; // Changed age from 18 to 20
personProxy.name = "HearLing"; // Changed name from Hearling to HearLing

小结

不仅仅是 getset,理解代理的核心,会发现代理还有很多用处:

1、代理可用于添加验证。

用户不应该能够将人的年龄更改为字符串值,或者给他一个空名称。或者如果用户试图访问对象上不存在的属性,我们应该让用户知道。

2、代理确保我们没有用错误的值修改 person 对象,这有助于我们保持数据的纯净!

3、代理可以有各种用例:它可以帮助验证、格式化、通知或调试。

tips:过度使用 Proxy 对象或对每个处理程序方法调用执行繁重的操作容易对应用程序的性能产生负面影响。