0%

关于代理模式

定义

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

分类

  • 事件代理
  • 虚拟代理
  • 缓存代理
  • 保护代理

事件代理

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

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

1
// 假如不用代理模式,我们将循环安装监听函数
2
const aNodes = document.getElementById('father').getElementsByTagName('a')
3
  
4
const aLength = aNodes.length
5
for(let i=0;i<aLength;i++) {
6
    aNodes[i].addEventListener('click', function(e) {
7
        e.preventDefault()
8
        alert(`我是${aNodes[i].innerText}`)                  
9
    })
10
}

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

1
// 使用事件代理的实现方式
2
// 获取父元素
3
const father = document.getElementById('father');
4
5
// 给父元素安装一次监听函数
6
father.addEventListener('click', function(e) {
7
    // 识别是否是目标子元素
8
    if(e.target.tagName === 'A') {
9
        // 以下是监听函数的函数体
10
        e.preventDefault();
11
        alert(`我是${e.target.innerText}`);
12
    }
13
});

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

虚拟代理

有时候,为了减少不必要的实例化对象的开销,避免造成资源的浪费,我们会把实例化操作延后执行,这就是所谓的虚拟代理。

一个常见的应用场景就是图片的懒加载:

1
<template>
2
  <img v-if="!lazy"
3
       class="ym-image-component"
4
       :src="source"
5
       :alt="alt"
6
       @click="handleClick"
7
  >
8
  <img v-else
9
       v-lazy="source"
10
       :alt="alt"
11
  >
12
</template>
13
<script>
14
  export default {
15
    name: 'YmImg',
16
    props: {
17
      src: {
18
        type: String,
19
        require: true,
20
        default: require('@/assets/img/default/loading.gif'), // eslint-disable-line global-require
21
      },
22
      alt: {
23
        type: String,
24
        default: '图片',
25
      },
26
      lazy: {
27
        type: Boolean,
28
        default: false,
29
      },
30
    },
31
    computed: {
32
      source() {
33
        const { src } = this;
34
        if (src && src !== '') return src;
35
        return require('@/assets/img/default/loading.gif'); // eslint-disable-line global-require
36
      },
37
    },
38
    methods: {
39
      handleClick() {
40
        this.$emit('click');
41
      },
42
    },
43
  };
44
</script>

缓存代理

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

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

1
// addAll方法会对你传入的所有参数做求和操作
2
const addAll = function() {
3
    console.log('进行了一次新计算');
4
    let result = 0;
5
    const len = arguments.length;
6
    for(let i = 0; i < len; i++) {
7
        result += arguments[i];
8
    }
9
    return result;
10
};
11
// 为求和方法创建代理
12
const proxyAddAll = (function(){
13
    // 求和结果的缓存池
14
    const resultCache = {};
15
    return function() {
16
        // 将传入的参数转化为一个唯一的字符串
17
        const args = Array.prototype.join.call(arguments, ',');
18
        
19
        // 检查本次参数是否有对应的计算结果
20
        if(args in resultCache) {
21
            // 如果有,则返回缓存池里现成的结果
22
            return resultCache[args];
23
        }
24
        return resultCache[args] = addAll(...arguments);
25
    }
26
})();


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

保护代理

前置知识: ES6中的Proxy

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

1
const proxy = new Proxy(obj, handler);

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

“婚介所”的实现
1
// 未知妹子
2
const girl = {
3
  // 姓名
4
  name: '小红',
5
  // 自我介绍
6
  aboutMe: '...',
7
  // 年龄
8
  age: 24,
9
  // 职业
10
  career: 'teacher',
11
  // 假头像
12
  fakeAvatar: 'xxxx',
13
  // 真实头像
14
  avatar: 'xxxx',
15
  // 手机号
16
  phone: 123456,
17
};

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

1
// 普通基本信息
2
const baseInfo = ['age', 'career']
3
// 最私密信息
4
const privateInfo = ['avatar', 'phone']
5
// 用户 对象实例
6
const user = {
7
    ...(一些必要的个人信息)
8
    isValidated: true,
9
    isVIP: false,
10
}
11
// 婚介所登场了
12
const JuejinLovers = new Proxy(girl, {
13
  get: function(girl, key) {
14
      if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
15
          alert('您还没有完成验证哦');
16
          return;
17
      }
18
      
19
      //...(此处省略其它有的没的各种校验逻辑)
20
    
21
      // 此处我们认为只有验证过的用户才可以购买VIP
22
      if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
23
          alert('只有VIP才可以查看该信息哦');
24
          return;
25
      }
26
  },
27
});