JavaScript中的代理模式

121 阅读5分钟

1.定义

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

例子: 小明追女神的故事:小明想送花给女神A,内向的小明让同学B代替自己送花 同学B就是一个代理

2.保护代理和虚拟代理

保护代理: 保护代理用于控制不同权限的对象对目标对象的访问,但在 JavaScript 并不容易实现保护代理,因为我们无法判断谁访问了某个对象。

虚拟代理 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建,例如同学B在女神A开心时才去帮小明买花给女神A

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

业务背景:图片预加载,图片太大先设置一个loading占位图,等图片加载完在替换 不适用虚拟代理写法

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

  var img = new Image();
  img.onload = function () {
    imgNode.src = img.src;
  };

  return {
    setSrc: function (src) {
      imgNode.src = "https://www.zuiaiwanqian.com/images/test.png";
      img.src = src;
    },
  };
})();
myImage.setSrc("https://www.zuiaiwanqian.com/images/preview-photo.gif")

可以实现图片预加载功能,但是该方法不满足面向对像设计的原则中的单一职责原则

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。

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

解析 在上面这个方法中,除了负责给img设置图片,还负责了预加载,如果之后要把预加载功能去掉,就不得不改动myImage对象,这样子又违反了开放-封闭原则

所以可以把预加载放在代理中实现,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体

虚拟代理写法

// 本体
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("https://www.zuiaiwanqian.com/images/test.png");
      img.src = src;
    },
  };
})();

proxyImage.setSrc("https://www.zuiaiwanqian.com/images/preview-photo.gif");

纵观整个程序,我们并没有改变或者增加myImage 的接口,但是通过代理对象,实际上给系统添加了新的行为。这是符合开放—封闭原则的。给img 节点设置src 和图片预加载这两个功能,被隔离在两个对象里,它们可以各自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可

4.代理和本地接口的一致性

上一个示例中,代理对象和本地都对外提供了setSrc方法,在用户看来,代理对象和本体是一致的, 代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样子有两个好处

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

5.虚拟代理合并HTTP请求

业务场景:点击checkbox发送请求给后端,可以把请求合并在一起,2s后一起发送 未使用虚拟代理

var synchronousFile = function (id) {
  console.log("开始同步文件,id为" + id);
};
var checkbox = document.querySelectorAll("input");
for (var i = 0; i < checkbox.length; i++) {
  checkbox[i].onclick = function () {
    if (this.checked === true) {
      synchronousFile(this.id);
    }
  };
}

以上代码如果用户点击checkbox速度很快1s可以点5个,这样子就会连续发送5个HTTP请求,造成服务器压力,可以使用虚拟代理将请求合并在一起2s后一块发送 使用虚拟代理

// 本体
var synchronousFile = function (id) {
  console.log("开始同步文件,id为" + id);
};
// 虚拟代理对象
var proxySynchronousFile = (function () {
  var cache = [];
  var timer = null;
  return function (id) {
    cache.push(id);
    if (timer) {
      return;
    }
    timer = setTimeout(() => {
      synchronousFile(cache.join(","));
      clearTimeout(timer);
      timer = null;
      cache.length = 0; // 清空缓存
    }, 2000);
  };
})();
var checkbox = document.querySelectorAll("input");
for (var i = 0; i < checkbox.length; i++) {
  checkbox[i].onclick = function () {
    if (this.checked === true) {
      synchronousFile(this.id);
    }
  };
}

6.虚拟代码在惰性加载的应用

如果需要加载一个js文件,但是在没有加载之前可以调用这个js文件中的API,这个时候就可以通过一个这个js的代理对象去缓存调用的API,等需要加载这个js时,再去这个缓存里面拿到这些方法依次调用

7.缓存代理

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

  1. 计算乘积的函数
  2. 使用缓存代理,将出现过的值缓存下来
// 本体
var mult = function () {
  console.log("开始计算乘积");
  var a = 1;
  for (let i = 0; i < arguments.length; i++) {
    a = a * arguments[i];
  }
  return a;
};
// 缓存代理对象
var proxyMult = (function () {
  var cache = {};
  return function () {
    var args = Array.prototype.join.call(arguments, ",");
    console.log(cache);
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
})();
console.log(proxyMult(1, 2, 3));
console.log(proxyMult(1, 2, 3));

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

通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。创建一个缓存代理工厂,可以往里面加入不同的函数,如加法,减法

/**************** 计算乘积 *****************/ 
var mult = function(){ 
    var a = 1; 
    for ( var i = 0, l = arguments.length; i < l; i++ ){ 
        a = a * arguments[i];  
    } 
    return a; 
}; 
 
/**************** 计算加和 *****************/ 
var plus = function(){ 
    var a = 0; 
    for ( var i = 0, l = arguments.length; i < l; 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 ); 
 
alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24 
alert ( proxyMult( 1, 2, 3, 4 ) );    // 输出:24 
alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10 
alert ( proxyPlus( 1, 2, 3, 4 ) );    // 输出:10

总结

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