前言
今天想要来介绍设计模式中的Proxy Pattern(代理者模式)。在日常生活中,我们有可能会因为许多原因,没有办法直接和对方谈话,于是就会经过他的代理人或是助理传达信息(就像如果你要约坤坤切磋球艺,就得经过他的经纪人一样)。
代理模式就像上面的例子一样,我们会在目标对象上多包装一层代理,通过代理来访问目标对象,而不是直接访问。在 JavaScript 中,我们已经有现成的语法可以使用代理了,本文就来看看如何巧妙地运用代理吧!🙌
实践
如果有一个时长两年半的练习生对象,其中的 sports 属性记录了学生喜欢的运动,还有 addSport 和 removeSport 方法用来操作 sports:
js
//student.js
let student = {
name: 'ikun',
sports: ['唱', '跳','rap','篮球'],
addSport: function(sport) {
this.sports = [
...this.sports,
sport,
];
},
removeSport: function(targetSport) {
this.sports = this.sports
.filter(sport => sport !== targetSport);
},
};
关于界面方面,练习生可以通过复选框选择自己喜欢的运动,并在选择后将该运动添加或从运动列表中删除。一切看起来都很完美。
直到有一天,老板突然提出了一个要求,希望我们列表上的运动选项不再是静态的,而是能够动态地让练习生自由地添加新的运动选项。这个需求非常合理,于是我们在addSport函数中增加了一个检查功能,使得目前不存在的运动选项可以被动态地添加进来
js
let student = {
// ...
addSport: async function(sport) {
const isSportExist = await api.checkIsSportExist(sport);
if (!isSportExist) {
api.storeSport(sport);
}
this.sports = [
...this.sports,
sport,
];
},
// ...
};
但是这样做会导致 addSport 承担了超出自身职责的逻辑,而且如果以后需要暂停或修改相关行为,addSport 就会变得越来越复杂,也容易导致原本的逻辑出错,这可如何是好?
那我们可以不修改 addSport,却又增加它的行为吗?那就是我们的主角Proxy Pattern
!
Proxy Pattern(代理者模式)
在代理模式中,我们将要访问的真实对象称为RealSubject,而代理RealSubject的那一层就称为Proxy。我们可以很轻松地使用JavaScript提供的现有语法来实现代理:
js
let target = {};
const handler = {
get(target, prop, receiver) {
console.log({ target, prop, receiver });
return Reflect.get(target, prop);
},
set(target, prop, value, receiver) {
console.log({ target, prop, value, receiver });
return Reflect.set(target, prop, value);
},
}
let proxy = new Proxy(target, handler);
在创建 Proxy 对象时,需要指定 target
和 handler
。target 就是之前提到的 RealSubject,而 handler 则会包含一些在对 RealSubject 进行操作时需要添加的额外逻辑。get 和 set 是 Proxy 中最基本的两个功能,如果想了解更多方法,可以参考 MDN
对于上述代码示例来说,当我想要通过代理为目标对象增加一个id时,handler中的set方法会先被执行,而在执行set方法时会接收以下几个参数:
- target:就是 RealSubject。
- prop:要 set 值的 prop 名称。
- value:要 set 的值。
- receiver:proxy 本身。
另外要提的是 Reflect,在 set 里面使用的 Reflect.set(target, prop, value)
就相当于 target[prop] = value
。
除了set以外,handler里还有get可以用,接收的参数和set差不多,只是会在取值的时候被调用,以及get的参数中没有value而已
大概了解一下代理(Proxy)的用法后,就可以来改写一下上方练习生(student)的例子了:
js
let student = {
sports: ['唱', '跳','rap'],
addSport: function(sport) {
this.sports = [
...this.sports,
sport,
];
},
// ...
};
// Mock Api
let api = {
checkIsSportExist: async (sport) => {
console.log('checkIsSportExist');
return false;
},
storeSport: async (sport) => {
console.log('already stored!');
}
}
const checkIsSportExistAndStore = async (sport) => {
const isSportExist = await api.checkIsSportExist(sport);
if (!isSportExist) {
api.storeSport(sport);
}
};
const studentHandler = {
get(target, prop) {
if (prop === 'addSport') {
checkIsSportExistAndStore();
}
return Reflect.get(target, prop);
}
}
let studentProxy = new Proxy(student, studentHandler);
看起来上面的代码突然变得有点多,不过让我们逐一分析一波:
- 练习生和原来一样,没有变化
- api 那段是用来模拟调用 API 的。
- 将检查和新增的逻辑提取到checkIsSportExistAndStore方法中
- 创建一个 studentHandler,如果 addSport 被调用,就再执行一个 checkIsSportExistAndStore
最后,我们创建了一个名为"student"的代理人,并执行了以下结果:
当通过 studentProxy 执行 addSport 时,可以明显地看到 checkIsSportExist 和 storeSport 都被执行,而且 addSport 之前的方法也都正常,练习生又可以愉快的练习篮球了。
其他案例
另一个我没有实际使用过,但一直迫不及待想试试的例子是将代理用于缓存。举个例子,如果一个方法内部的逻辑运算,或者在执行时需要花费相当时间的 API,那么就可以使用缓存代理:
js
const analysisApi = {
getAnalysis: (data) => {
return new Promise((res) => {
console.log('getAnalysis');
setTimeout(() => {
res(`fetched: ${data}`);
}, 3000);
});
},
};
const analysisApiHandler = {
analysisCache: {},
get(target, prop) {
let thisAnalysisCache = this.analysisCache;
if (prop === 'getAnalysis') {
return async function(...args) {
const apiKey = args.join('');
if (thisAnalysisCache[apiKey]) {
return thisAnalysisCache[apiKey];
}
const result = await Reflect.get(target, prop).apply(this, args);
thisAnalysisCache[apiKey] = result;
return result;
};
}
return Reflect.get(target, prop);
},
};
const analysisApiProxy = new Proxy(analysisApi, analysisApiHandler);
这段代码分为两个部分,一个是用来处理 API 请求的 analysisApi,我们假设其中的 getAnalysis 需要等待一段较长的时间才能得到结果,所以使用 setTimeout 模拟请求的时间
另一部分是代理的处理方法,首先定义一个 analysisCache 的对象,用来记录在此参数下会得到的对应结果。第二步是在 get 方法中的逻辑,首先将 analysisCache 取出放到 thisAnalysisCache 中,以避免在执行时 this 指向被操作的代理,而不是 analysisApiHandler
然后判断如果执行的方法是 getAnalysis 的话,就返回一个新的方法,此方法会将这次的参数用 join 的方式组合成 apiKey,再确认 thisAnalysisCache 中有没有记录 apiKey 的结果,有的话就直接返回,没有的话就使用 apply 执行,并将这次的 apiKey 及对应的结果存到 thisAnalysisCache 中,这样下次再用相同参数调用 getAnalysis 就可以直接返回 thisAnalysisCache 内的记录,不需要再等待那么久了
执行结果如下,只要是相同的参数就不会进入真正的 getAnalysis:
第二次使用参数 `rap` 就不会进入 getAnalysis 了
然而,使用缓存的缺点在于,如果数据经常被更新,那么缓存反而会导致用户无法及时获取到正确的数据。因此,在使用时必须注意场景是否合适
在不修改原有对象的情况下,通过附加一些控制逻辑,它真的非常实用!除了本文提到的应用之外,代理模式还有很多其他类型和场景可以使用。如果将来在工作中用到了,我会再与大家分享我的经验!
如果文章中有任何错误的地方,欢迎留言交流一起学习!