前端须知——结构型模式——代理模式

201 阅读8分钟

代理模式

代理模式属于设计模式中结构型的设计模式;

定义

为一个对象提供一种代理以控制对这个对象的访问

白话解释:   很多明星都是有经纪人的,如果要联系明显进行商演或者开演唱会之类的商业活动通过是需要先跟经纪人取得联系的,跟经纪人谈好了合作事宜之后经纪人再转达给某明星,然后某明星才会去参加活动;同样租房也是一个同样的道理,我们不管是租房还是买房,第一反应肯定是找链家这类的平台,因为我们只需要跟链家进行沟通,而链家去跟房东沟通,省去了我们直接和房东沟通的步骤;因为链家就是一个代理模式,它代理了这个房东的房源。

详解

代理

你作为一个追星狂魔,是某明星的忠诚粉丝;刚好某明星近期要过生日了,你准备送上礼物代表你的心意,正常的流程:

const Fans = {
    flower(){
        star.reception("花");
    }
}

const star = {
    reception:function(gift){
        console.log("收到粉丝的:"+gift);
    }
}

Fans.flower();   //收到粉丝的:花

  你选择了买花寄给她,希望她能感受到你的心意;但是往往理想很丰满,现实很骨感!别忘了还有经纪人,因为签收你的礼物的往往不是明星本人而是经纪人:

const Fans = {
    flower(){
        Agent.reception("花");
    }
}

const Agent = {
    reception:function(gift){
        console.log("粉丝送的:"+gift);   //粉丝送的:花
        star.reception("花");
    }
}
const star = {
    reception:function(gift){
        console.log("收到粉丝的:"+gift);
    }
}

Fans.flower();    //收到粉丝的:花

  这里的经纪人就是一个简单的代理了,粉丝需要先把礼物给经纪人,经纪人再转给明星本人;

保护代理:

明星满心欢喜的看到粉丝寄过来的包裹的时候,拆开一看,原来是花!明星很不屑,所以告诉经纪人,以后凡是给我寄花的,通通不要给我了,你自己看着处理:

const Fans = {
    flower(){
        Agent.reception("花");
    }
}

const Agent = {
    reception:function(gift){
        console.log("粉丝送的:"+gift);  //粉丝送的:花
        if(gift != "花"){
            star.reception("花");
        }

    }
}
const star = {
    reception:function(gift){
        console.log("收到粉丝的:"+gift);
    }
}

Fans.flower();

  上面的程序中明星根本就没有收到粉丝寄过来的花,因为在经纪人那里就已经拦截处理了;   通过经纪人来过滤掉一部分礼物,这种模式叫做保护代理;

虚拟代理:

  粉丝送花明星收不到,那粉丝就转换一下思路,送点钱自己去买想要的东西吧!于是找到经纪人,给了经纪人一百万现金,让经纪人转达给明星本人;

function Money(){
    this.total = "一百万现金"
   	return this.total;
}
const Fans = {
    flower(){
        Agent.reception();
    }
}

const Agent = {
    reception:function(){
        // console.log("粉丝送的:"+gift);
        let money = new Money();
        star.reception(money.total);

    }
}
const star = {
    reception:function(gift){
        console.log("收到粉丝的:"+gift);  //收到粉丝的:一百万现金
    }
}

Fans.flower();

  明星收到了一百万就很开心;这一百万因为不是花,没有被经纪人拦截过滤;所以明星本人就直接收到了,这种模式我们称为虚拟代理模式;

虚拟代理实现图片懒加载:

没用代理的时候我们的代码是这样的

// 创建一个本体对象
const myImage = (function(){
  // 创建标签
  const imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

myImage.setSrc( 'xxx.jpg' );

使用虚拟代理后

// 创建一个本体对象
const myImage = (function(){
  // 创建标签
  const imgNode = document.createElement( 'img' );
  // 添加到页面
  document.body.appendChild( imgNode );
  return {
    // 设置图片的src
    setSrc: function( src ){
      // 更改src
      imgNode.src = src;
    }
  }
})();

// 创建代理对象
const proxyImage = (function(){
  // 创建一个新的img标签
  const img = new Image;
  // img 加载完成事件
  // img 具有src之后,onload事件触发
  img.onload = function(){
    // 调用 myImage 替换src方法
    myImage.setSrc( this.src );
  }
  return {
    // 代理设置地址
    setSrc: function( src ){
      // 预加载 loading
      myImage.setSrc( 'loading.gif' );
      // 赋值正常图片地址
      img.src = src;
    }
  }
})();

proxyImage.setSrc( 'xxx.jpg' );

虚拟代理同步数据

我们经常会出现选中某项内容同步到服务器 但问题是这样每次点击都会向服务器发送一次请求 网络开销就会非常大 我们现在就来模拟这种情况

<input type="checkbox" id="A">内容一<br>
<input type="checkbox" id="B">内容二<br>
<input type="checkbox" id="C">内容三<br>
<input type="checkbox" id="D">内容四<br>
<input type="checkbox" id="E">内容五<br>
<input type="checkbox" id="F">内容六<br>
<input type="checkbox" id="G">内容七<br>
<input type="checkbox" id="H">内容八<br>

在这里插入图片描述

function synData(ID){
    console.log(ID + '正在同步到服务器...');
}
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
    item.onclick = function(){ //为每一项绑定点击事件
        if(this.checked){ //选中状态则同步数据
            synData(this.id);
        }
    };
}

在这里插入图片描述

如果你的手速非常快,在2s内可以向服务器发送8次请求 多次HTTP请求十分耗性能, 你快了,客户端和服务器谁都受不了 所以我们引入一个虚拟代理 功能是防止你的操作过于频繁,把你的请求合并到一起发送给服务器 我们设置频率是2s一次,这样的速度服务器还是可以接受的

function synData(ID){
    console.log(ID + '正在同步到服务器...');
}
var proxySynData = (function(){
    var cache = [], //缓存我们需要同步的内容(待改进)
        timer; //定时器
    return function(ID){
        if(!timer){ //定时器不存在就创建
            timer = setTimeout(function(){
                synData(cache.join()); //同步合并后的数据
                cache.length = 0; //清空缓存
                clearTimeout(timer); //清除定时器
                timer = null; //方便垃圾回收
            }, 2000);
        }
        cache.push(ID); //存入缓存
    }
})();
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
    item.onclick = function(){
        if(this.checked){
            proxySynData(this.id);
        }
    };
}

这样每过2s就会向服务器同步一次数据 在这里插入图片描述

但是测试过程中我还是发现了问题 我多次点击同一个会重复同步像这样

在这里插入图片描述

这是因为我们数组数据结构的可重复性 我们可以利用对象属性的单一性来修改代码 完整代码如下

var synData = function(ID){
    console.log(ID + '正在同步到服务器...');
}
var proxySynData = (function(){
    var cache = {}, //改用对象作为缓存载体
        timer;
    return function(ID){
        if(!timer){
            timer = setTimeout(function(){
                synData(Object.keys(cache).join()); //改
                cache = {}; //改
                clearTimeout(timer);
                timer = null;
            }, 2000);
        }
        cache[ID] = 1; //我们不关注“值”,只关注“键”
    }
})();
var list = document.getElementsByTagName('input');
for(var i = 0, item; item = list[i]; i++){
    item.onclick = function(){
        if(this.checked){
            proxySynData(this.id);
        }
    };
}

这样的话,即使你是最强王者的手速,也最多2s同步一次数据 并且不会重复同步

缓存代理

缓存代理就很好理解了,可以缓存一些开销很大的运算结果

如果你第二次执行函数的时候,传递了同样的参数,那么就直接使用缓存的结果

如果运算量很大,这可是不小的优化

还是简单举个例子 写一个简单的计算加法的函数

const add = function(){
    let sum = 0;
    for(let i = 0, l = arguments.length; i < l; i++){
        sum += arguments[i];
    }
    return sum;
};
console.log(add(1,2,3,4,5)); //15

再写一个缓存代理,缓存运算结果 完整代码如下(为了证明我们使用了缓存,额外加了控制台打印信息)

const add = function(){
    let sum = 0;
    for(let i = 0, l = arguments.length; i < l; i++){
        sum += arguments[i];
    }
    return sum;
};
const proxyAdd = (function(){
    const cache = {}; //缓存运算结果的缓存对象
    return function(){
        let args = Array.prototype.join.call(arguments);//把参数用逗号组成一个字符串作为“键”
        if(cache.hasOwnProperty(args)){//等价 args in cache
            console.log('使用缓存结果');
            return cache[args];//直接使用缓存对象的“值”
        }
        console.log('计算结果');
        return cache[args] = add.apply(this,arguments);//使用本体函数计算结果并加入缓存
    }
})();
console.log(proxyAdd(1,2,3,4,5)); //15
console.log(proxyAdd(1,2,3,4,5)); //15
console.log(proxyAdd(1,2,3,4,5)); //15

在这里插入图片描述

动态创建代理

使用高阶函数动态的创造出代理函数更加灵活 修改我们上面写过的缓存代理 我们来制造一个“工厂” 这个“工厂”用来创造代理函数

const add = function(){
    let sum = 0;
    for(let i = 0, l = arguments.length; i < l; i++){
        sum += arguments[i];
    }
    return sum;
};
const proxyFnFactory = function(fn){
    const cache = {};
    return function(){
        let args = Array.prototype.join.call(arguments);
        if(args in cache){
            return cache[args];
        }
        return cache[args] = fn.apply(this,arguments);
    }
};
var proxyAdd = proxyFnFactory(add);
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));
console.log(proxyAdd(1,2,3,4,5));

通过这个“工厂”,我们还可以制造乘法缓存代理、阶乘缓存代理… 道理是一样的,就不再赘述了

其他代理

代理模式很多,不一定每一种都适用于我们JavaScript 了解一下

  • 防火墙代理:控制网络资源访问,很像保护代理
  • 远程代理:为一个对象在不同地址空间提供局部代表
  • 智能引用代理:访问对象时执行一些附加操作,比如计算对象访问次数
  • 写时复制代理:延迟复制(庞大)对象,对象被真正修改时才复制,很像虚拟代理

代理模式分析举例

表单验证例子

  保护代理就是起到保护作用,用来过滤掉一下不必要的请求,将真正需要的递给本体。

譬如,验证用户名是否合法

  这里我们应用,保护代理的思想,如果用户名是不合法的,则不会将该请求给本体执行
发送ajax请求

这样编写代码,的确能够完成业务的需求,能够完成表单的验证,但是存在很多问题

  1. 绑定的函数比较庞大,包含了很多的if-else语句,看着都恶心,这些语句需要覆盖所有的校验规则。
  2. 绑定的函数缺乏弹性,如果增加了一种新的校验规则,或者想要把密码的长度校验从6改成8,我们都必须深入
  3. 绑定的函数的内部实现,这是违反了开放-封闭原则的。
  4. 算法的复用性差,如果程序中增加了另一个表单,这个表单也需要进行一些类似的校验,那我们很可能将这些校验逻辑复制得漫天遍野。

关于这一点,我们可以去看 设计模式中的 “策略模式”

总结

我们常用的代理模式就是虚拟代理(十分重要)和缓存代理 编程的时候我们不用刻意去写代理

不能滥用模式,有时候仅仅是给代码增加复杂度了

什么时候需要用,什么时候写 不会影响你的其他代码