理解代理模式,很形象的例子就是明星的经纪人。

我希望能够让某位明星成为我的代言人,但是一般情况只能跟经纪人洽谈。而不会跟明星直接联系。

经纪人就是明星的代理。

生活中也有许多代理模式的案例。

例如我想购买飞机票,但是我可以不用直接在机场购买,我们小区楼下就有一家代理出售机票火车票的小机构。

解决跨域问题也可以使用代理。

浏览器中某一页面无法直接访问其他域名的接口。那如果就是有需求要访问其他域名的接口怎么办呢?

我在自己域名服务器下,创建一个代理。

浏览器访问自己域名服务器的代理接口,代理接口在服务端,不在浏览器,就不会有跨域限制,于是服务端的代理接口可以去访问其他域名的数据。这样就达到了目的。

代理模式的好处,在明星经纪人这里体现的非常明显。

想要找赵丽颖谈商业合作的商家非常多,但是如何对比开价更高的商家,如何淘汰恶意捣乱的商家,如何应付诚意不足的商家,如何安排通告行程等等,这些乱七八糟的事情都由经纪人来处理。

而赵丽颖只需要确定最终选择商家即可。代理模式让她避免了很多杂务。

在我们的代码设计中,代理模式,是为其他对象提供一种代理以控制对该对象的访问。

  • 代理对象,是对目标对象的一次封装
  • 代理对象可预先处理访问请求,再决定是否转交给目标对象
  • 代理对象和目标对象,对外提供的可操作性方式保持一致

针对不同的场景,实现代理的方式可能不一样。

在 JavaScript 中,提供了默认的 Proxy 对象,用于创建一个对象的代理。

code.ts
1
const t = {m: 1}
2
const p1 = new Proxy(t, {
3
get: function(obj, prop) {
4
return obj[prop]
5
}
6
})
7
8
// 通过代理对象,访问目标对象
9
console.log(p1.m) // 1
10
11
// 通过代理对象修改目标对象
12
p1.m = 2
13
console.log(t) // {m: 2}

Proxy 的第一个参数为目标对象,第二参数是监听事件,可以监听代理对象的访问与修改操作。这样的话,我们就可以利用代理对象,对目标对象的访问进行数据的劫持。

Vue3.0 正是利用了 Proxy 这样的特点,才能得以使用 Proxy 替换掉 getter/setter。

我们也可以自己实现一个 Proxy 对象。

INFO

ProxyPolyfill 简化版,仅仅只提供了 Object 的兼容。仅供参考阅读

code.ts
1
class Internal {
2
constructor(target, handler) {
3
this.target = target
4
this.handler = handler
5
}
6
get(property, receiver) {
7
var handler = this.handler;
8
if (handler.get ==undefined) {
9
return this.target[property];
10
}
11
if (typeof handler.get === 'function') {
12
return handler.get(this.target, property, receiver);
13
}
14
}
15
set(property, value, receiver) {
16
var handler = this.handler;
17
if (handler.set == undefined) {
18
this.target[property] = value;
19
} else if (typeof handler.set === 'function') {
20
var result = handler.set(this.target, property, value, receiver);
21
if (!result) {
22
console.error(`set 异常: ${property}`)
23
}
24
} else {
25
console.error("Trap 'set' is not a function: " + handler.set);
26
}
27
}
28
}
29
30
function ProxyPolyfill(target, handler) {
31
return proxyObject(new Internal(target, handler));
32
}
33
34
/**
35
* Proxy object 这里是核心关键,使用 Object.create 的方式与 目标对象建立绑定关系
36
* @param {Internal} internal
37
* @returns {object}
38
*/
39
function proxyObject(internal) {
40
var target = internal.target;
41
var descMap, newProto;
42
43
descMap = observeProto(internal);
44
newProto = Object.create(Object.getPrototypeOf(target), descMap);
45
46
descMap = observeProperties(target, internal);
47
return Object.create(newProto, descMap);
48
}
49
50
/**
51
* Observe [[Prototype]]
52
* @param {Internal} internal
53
* @returns {object} descriptors
54
*/
55
function observeProto(internal) {
56
var descMap = {};
57
var proto = internal.target;
58
while (proto = Object.getPrototypeOf(proto)) {
59
var props = observeProperties(proto, internal);
60
Object.assign(descMap, props);
61
}
62
descMap.__PROXY__ = {
63
get: function () {
64
return internal.target ? undefined : 'REVOKED';
65
}
66
};
67
return descMap;
68
}
69
70
/**
71
* Observe properties
72
* @param {object} obj
73
* @param {Internal} internal
74
* @returns {object} descriptors
75
*/
76
function observeProperties(obj, internal) {
77
var names = Object.getOwnPropertyNames(obj);
78
var descMap = {};
79
for (var i = names.length - 1; i >= 0; --i) {
80
descMap[names[i]] = observeProperty(obj, names[i], internal);
81
}
82
return descMap;
83
}
84
85
/**
86
* Observe property,让 代理对象的属性操作,映射到目标对象
87
* @param {object} obj
88
* @param {string} prop
89
* @param {Internal} internal
90
* @returns {{get: function, set: function, enumerable: boolean, configurable: boolean}}
91
*/
92
function observeProperty(obj, prop, internal) {
93
var desc = Object.getOwnPropertyDescriptor(obj, prop);
94
return {
95
get: function () {
96
return internal.get(prop, this);
97
},
98
set: function (value) {
99
internal.set(prop, value, this);
100
},
101
enumerable: desc.enumerable,
102
configurable: desc.configurable
103
};
104
}

简单验证一下,发现初步达到了目的

code.ts
1
const t = {m: 1}
2
const p1 = new ProxyPolyfill(t, {
3
get: function(obj, prop) {
4
return obj[prop]
5
}
6
})
7
8
p1.m = 2
9
console.log(t) // {m: 2}

虚拟代理:图片加载

有的时候图片过大,在页面上不能够快速的完整显示。这个时候体验就非常不好。

当图片还没有完整加载完成时,我们可以使用一个默认图片或者 loading 图片进行占位。目标图片加载完成之后,再将 loading 图片替换成目标图片。

code.ts
1
var targetImage = (function () {
2
var imgNode = document.createElement('img');
3
document.body.appendChild(imgNode);
4
return {
5
setSrc: function (src) {
6
imgNode.src = src;
7
}
8
}
9
})();
10
11
var proxyImage = (function() {
12
var img = new Image();
13
// 先加载 loading 或者默认图片用于快速显示
14
targetImage.setSrc('loading.gif')
15
img.onload = img.onerror = function () {
16
// 加载完成之后,替换目标图片
17
targetImage.setSrc(img.src);
18
};
19
20
return {
21
setSrc: function (src) {
22
// 此时开始加载图片
23
img.src = src;
24
}
25
}
26
})();
27
28
proxyImage.setSrc('https://cn.bing.com/sa/simg/hpb/LaDigue_EN-CA1115245085_1920x1080.jpg');

缓存代理:记忆函数

有这样一个函数。该函数接收两个整数参数,第一个参数表示开始数字,第二个参数表示结束数字,该函数的功能是计算开始到结束的范围中,所有整数的和。

code.ts
1
function sum(start, end) {
2
let res = 0
3
for (let i = start; i <= end; i++) {
4
res += i
5
}
6
return res
7
}

于是问题来了,如果是大额的计算,计算成本很高。例如,我多次调用该方法,计算 1 ~ 100000 的和。

code.ts
1
sum(1, 100000)
2
sum(1, 100000)
3
sum(1, 100000)
4
sum(1, 100000)

我们仔细分析一下,会发现有大量的冗余计算过程。1 ~ 100000 的计算,会被重复计算很多次。

既然都是计算 1 ~ 100000,那能不能优化一下,只计算一次就好了?

当然可以。

我们联想一下纯函数的特点,相同的输入,总能得到相同的输出。因此,如果我们能判断,输入的参数是相同的,那么就可以直接上上一次的计算结果返回出来。

优化方案如下:

code.ts
1
function withSum(base) {
2
const cache = {}
3
4
return (...args) => {
5
const str = `${args[0]}-${args[1]}`
6
if (!cache[str]) {
7
cache[str] = base.apply(null, args)
8
}
9
return cache[str]
10
}
11
}

使用时很简单

code.ts
1
const _sum = withSum(sum)
2
3
_sum(1, 100000)
4
_sum(1, 100000)
5
_sum(1, 100000)
6
_sum(1, 100000)

该方案利用了闭包,将计算结果缓存在 cache 字段中,当再一次需要计算时,就从闭包中先判断是否已经有计算结果了,如果有就直接返回结果,如果没有才会重新计算。

如果仅仅从计算成本来考虑,新的方案有两个成本,一个是比较的成本,一个是计算结果的成本。

通常情况下,比较成本都是远远低于计算成本的,因此能够达到提高计算速度的效果,但是有的情况下,比较时间可能会大于计算时间。

因此,在实践中运用此方法时,要明确好判断条件,确保判断条件都是简单的,如果太过于复杂的比较条件,也有可能导致没有起到优化的效果,反而消耗更大。

在 React 中,有许多用到了这种方法的场景。

例如 React.memo,useState,useCallback,useMemo 等等。

也就意味着,我们在 React 实际场景中,也要尽量简化比较条件。

代理的方式还有很多,根据不同的场景处理手段也不一样,这里就不再一一列举。

专栏首页
到顶
专栏目录