什么是代理模式

代理模式是:使用代理对象完成用户请求,屏蔽用户对真实对象的访问。

代理模式很好理解,“有事别找我,找我的代理去”这就是代理模式。我们在这里打个比方,现在有三个类,目标类、代理类、用户类,这代理模式中,用户类没办法直接与目标类进行沟通,如果用户想要联系目标类那怎么办呢? 好办,找代理类,把你的需求告诉代理类,然后再由代理类去联系目标类,最后能不能办,办成什么样,完全靠着代理类来做。这种形式就是代理模式。

举例说明

看了上面的解释之后,我们会不会感觉怪怪的,这样做有什么好处呢?用户类直接去联系目标类不就可以了,我们中间加这个代理有什么用?我们来看一个具体的实例,看完这个实例我们就明白了。

代理模式最常使用的例子就是明星与经纪人的例子了。对于明星来说他的联系方式是不会随便透露出来的,如果客户有什么需要则需要去联系的是明星的代理人,也就是经纪人,由经纪人来把所有的需求进行整理之后在通知给明星,在这里客户是不能也没有办法去直接联系到明星的。

这种通过代理人来联系目标的方式就是代理模式。

绘制UML类图

然后我们就根据上面明星与经纪人的例子,来绘制UML类图

在这里插入图片描述

这就是我们代理模式的UML类图,明星star,他拥有的一个工作的方法working,但是这个方法客户Client是没有办法去调用的,客户如果想要让明星工作,那么需要首先联系明星的代理人Agent,通过代理人的working方法,来让明星star去工作。

OK,接下来我们来实现一下代码。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Star {
working () {
console.log('明星开始工作');
}
}

class Adent {
constructor () {
this.star = new Star();
}

working () {
this.star.working();
}
}

class Client {
constructor () {
this.adent = new Adent();
}

main () {
this.adent.working();
}
};

const client = new Client();
client.main();

看一下我们的代码 ,首先是由一个明星的类Star,他拥有一个方法working,当调用working的时候,明星开始工作,然后是一个代理类Adent,它表示经纪人,经理人持有明星的引用,并且仅能有经纪人持有明星,经纪人也提供了一个working的方法,当经纪人的working被执行的时候,它会调用明星的working通知明星开始工作。

最后就是客户ClientClient持有一个经纪人(代理类)的引用,当他想要明星工作的时候,他需要通知经纪人,调用经纪人的working方法,然后经纪人会去通知明星开始工作。

这样我们就通过代码来实现了一个代理模式,还是比较简单的。然后我们看一下代理模式的使用场景。

使用场景

代理模式的使用场景比较典型的就是jQuery$.proxy()Vue中的_proxyData这两个方法了。我们这里以Vue中的_proxyData做场景演示,我们将通过Vue的源码来进行演示,不过在看Vue的源码之前,我们需要带着问题去看,_proxyData他有什么作用。

大家先来想一下,我们在new Vue的时候去传入了一个data的对象,执行的代码一般都是这样:

1
2
3
4
5
6
new Vue({
data: {
msg: 'Hello world'
},
....
});

那么这里的data作为一个独立的对象是如何被Vue去访问当的?我们在Vuecreated方法里面,通过this.data可以直接获取到这个对象,为什么?Vue究竟通过了什么样的方式,让我们可以通过this.data就能获取到data这个独立的对象呢?

我们之所以可以直接通过this.data来访问data对象,就是因为Vue在内部对data设置了代理,所使用的模式就是代理模式。OK,明白了这些,那么我们看一下Vue内部是怎么去做的。

我们打开Vue的源码,我这里的是2.5.16版本的,大家可以去github上去下载最新的版本哈,代码下载下来之后,我们打开src/core/instance/index.js

1
2
3
4
5
6
7
8
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

这就是当我们进行new Vue()之后执行的代码,我们声明Vue的时候传入的对象就是这里的 options,当我么执行options.data的时候,获取到的就是

1
2
3
data: {
msg: 'Hello world'
}

这个data对象。

然后在this._init(options)的方法里面Vue会调用initState(vm),进而在initState()中调用调用initData(vm),这个initData(vm)就是使用代理模式来实现的,我们一起看一下initData()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function initData (vm: Component) {
...
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
...
}

这里是initData()的代码实现,我们只截取出来和代理有关的代码,在这些代码里面,它首先获取到了所有datakey值,然后进行了一个遍历操作,遍历data中的所有字段,并调用了proxy(vm, "_data", key)这个方法,这个方法就是为data设置代理的方法,这里的vm就是Vue的实例,"_data"是一个字符串,key就是data对象中所有字段的key值。然后是proxy()的方法实现。

1
2
3
4
5
6
7
8
9
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}

这就是proxy的实现代码,在这里Vue就通过Object.defineProperty为每一个data的字段去设置了代理,这段代码这样看可能会有点复杂,我们把它合起来看一下:

1
2
3
4
5
6
7
8
Object.defineProperty(vm, key, {
get: function () {
return vm._data[key];
},
set: function (newVal) {
vm._data[key] = newVal;
}
});

就是这么一段代码,让我们可以直接通过this.data来获取到我们传入的data对象,在这里this.data就是代理对象,data就是目标对象。所有的流程代码被简化之后,就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Vue (options) {
var vm = this;
this.$options = options;
var data = this._data = options.data || {};
Object.keys(data).forEach(function(key) {
vm._proxyData(key);
});
}

Vue.prototype = {
_proxyData: function(key) {
var vm = this;
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function () {
return vm._data[key];
},
set: function (newVal) {
vm._data[key] = newVal;
}
});
},
};

总结

进入总结部分,在这一章我们学习到的是代理模式,在代理模式里面最核心的概念就是 目标类与用户不能直接联系,所有的联系必须通过代理来完成。 就好像明星与客户不会直接联系一样,他们所有的联系都会通过经纪人来完成。那么我们应该在什么情况下去使用代理模式呢?

比如当我们的目标类拥有很多的私有方法,但也有一些公开的属性或方法需要被外部调用,而在JavaScript中又没有限制现有方法不能被外部访问的功能,这个时候,我们就可以通过代理类,来把目标类中的公开属性或者方法代理出去,就好像Vue中访问data对象一样。