定义
在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个
第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。
分类
- 事件代理
- 虚拟代理
- 缓存代理
- 保护代理
事件代理
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 | }); |