关于代理模式

103 阅读4分钟

前情提要

在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个 第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。

分类

· 事件代理

· 虚拟代理

· 缓存代理

· 保护代理

举例:

事件代理

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>事件代理</title>
</head>
<body>
  <div id="father">
    <a href="#">链接1号</a>
    <a href="#">链接2号</a>
    <a href="#">链接3号</a>
    <a href="#">链接4号</a>
    <a href="#">链接5号</a>
    <a href="#">链接6号</a>
  </div>
</body>
</html>

我们现在的需求是,希望鼠标点击每个 a 标签,都可以弹出“我是xxx”这样的提示。比如点击第一个 a 标签,弹出“我是链接1号”这样的提示。这意味着我们至少要安装 6 个监听函数给 6 个不同的的元素(一般我们会用循环,代码如下所示),如果我们的 a 标签进一步增多,那么性能的开销会更大。

// 假如不用代理模式,我们将循环安装监听函数
const aNodes = document.getElementById('father').getElementsByTagName('a')
  
const aLength = aNodes.length

for(let i=0;i<aLength;i++) {
    aNodes[i].addEventListener('click', function(e) {
        e.preventDefault()
        alert(`我是${aNodes[i].innerText}`)                  
    })
}

考虑到事件本身具有“冒泡”的特性,当我们点击 a 元素时,点击事件会“冒泡”到父元素 div 上,从而被监听到。如此一来,点击事件的监听函数只需要在 div 元素上被绑定一次即可,而不需要在子元素上被绑定 N 次——这种做法就是事件代理,它可以很大程度上提高我们代码的性能。

// 使用事件代理的实现方式
// 获取父元素
const father = document.getElementById('father');

// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
    // 识别是否是目标子元素
    if(e.target.tagName === 'A') {
        // 以下是监听函数的函数体
        e.preventDefault();
        alert(`我是${e.target.innerText}`);
    }
});

在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。

缓存代理

缓存代理应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。

一个比较典型的例子,是对传入的参数进行求和:

// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
    console.log('进行了一次新计算');
    let result = 0;
    const len = arguments.length;
    for(let i = 0; i < len; i++) {
        result += arguments[i];
    }
    return result;
};

// 为求和方法创建代理
const proxyAddAll = (function(){
    // 求和结果的缓存池
    const resultCache = {};
    return function() {
        // 将传入的参数转化为一个唯一的字符串
        const args = Array.prototype.join.call(arguments, ',');
        
        // 检查本次参数是否有对应的计算结果
        if(args in resultCache) {
            // 如果有,则返回缓存池里现成的结果
            return resultCache[args];
        }
        return resultCache[args] = addAll(...arguments);
    }
})();

我们发现 proxyAddAll 针对重复的传参只会计算一次,这将大大节省计算过程中的时间开销。现在我们传入了4个参数,可能还看不出来,当我们针对大量参数、做反复计算时,缓存代理的优势将得到更充分的凸显。

保护代理

前置知识: ES6中的Proxy

在 ES6 中,提供了专门以代理角色出现的代理器 —— Proxy。它的基本用法如下:

const proxy = new Proxy(obj, handler);

第一个参数是我们的目标对象,也就是婚介所中的“未知妹子”。handler 也是一个对象,用来定义代理的行为,相当于“婚介所”。当我们通过 proxy 去访问目标对象的时候,handler 会对我们的行为作一层拦截,我们的每次访问都需要经过 handler 这个第三方。

“婚介所”的实现

// 未知妹子
const girl = {
  // 姓名
  name: '小红',
  // 自我介绍
  aboutMe: '...',
  // 年龄
  age: 24,
  // 职业
  career: 'teacher',
  // 假头像
  fakeAvatar: 'xxxx',
  // 真实头像
  avatar: 'xxxx',
  // 手机号
  phone: 123456,
};

婚介所收到了小红的信息,开始营业。大家想,这个姓名、自我介绍、假头像,这些信息基本信息,曝光一下没问题。但是人家妹子的年龄、职业、真实头像、手机号码,是不是属于非常私密的信息了?要想 get 这些信息,平台要考验一下你的诚意了 —— 首先,你是不是已经通过了实名审核?如果通过实名审核,那么你可以查看一些相对私密的信息(年龄、职业)。然后,你是不是 VIP ?只有 VIP 可以查看真实照片和联系方式。满足了这两个判定条件,你才可以顺利访问到别人的全部私人信息,不然,就劝退你提醒你去完成认证和VIP购买再来。

// 普通基本信息
const baseInfo = ['age', 'career']
// 最私密信息
const privateInfo = ['avatar', 'phone']

// 用户 对象实例
const user = {
    ...(一些必要的个人信息)
    isValidated: true,
    isVIP: false,
}

// 婚介所登场了
const JuejinLovers = new Proxy(girl, {
  get: function(girl, key) {
      if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
          alert('您还没有完成验证哦');
          return;
      }
      
      //...(此处省略其它有的没的各种校验逻辑)
    
      // 此处我们认为只有验证过的用户才可以购买VIP
      if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
          alert('只有VIP才可以查看该信息哦');
          return;
      }
  },
});