这是我参与更文挑战的第26天,活动详情查看:更文挑战
文章引导
代理模式篇幅较多,分为上、中、下三篇进行整理。
JavaScript实现代理模式(上篇)
内容梗概:主要介绍什么是代理模式,代理模式的简单实现
链接:juejin.cn/post/697850…
JavaScript实现代理模式(中篇)
内容梗概:分析代理模式的各种用途
链接:juejin.cn/post/697903…
JavaScript实现代理模式(下篇)
内容梗概:基于ES6 Proxy API实现更方便的代理模式
链接:未发布
使用代理模式中的保护代理
保护代理用于对象应该具有不同访问权限的场景,控制对原始对象的访问。
还是用上述书中的例子,因为小明和小姐姐的舍友是好朋友,她了解小明的为人,所以愿意为小明转送鲜花给小姐姐。
而如果把小明换成一个不相干的人,那么小姐姐的舍友不可能会答应这个奇怪的请求。
但是在JavaScript并不容易实现保护代理,因为我们无法判断谁访问了某个对象。所以我们对下面的例子进行简单的改造,给鲜花类增加来源,从而实现简单的保护代理。
// 鲜花类
class Flower {
constructor(source) {
this.source = source;
}
}
// 小明
let xiaoming = {
sendFlower(target){
let flower = new Flower('xiaoming');
target.receiveFlower( flower );
}
};
// 路人
let passerby = {
sendFlower(target){
let flower = new Flower('passerby');
target.receiveFlower( flower );
}
};
// 小姐姐的闺蜜
let ladybro = {
receiveFlower(flower){
if (flower.source === 'xiaoming') {
cuteGirl.listenGoodMood(() => { // 监听A的好心情
cuteGirl.receiveFlower( flower );
});
} else {
throw new Error('小姐姐的闺蜜拒绝帮你送花!')
}
}
};
// 小姐姐
let cuteGirl = {
receiveFlower( flower ){
console.log( '收到花 ' + flower );
},
listenGoodMood( fn ){
setTimeout(() => { // 假设10秒之后A的心情变好
fn();
}, 10000 );
}
};
// 小明将鲜花交给好朋友,委托好朋友在小姐姐心情好的时候将鲜花转交给小姐姐
xiaoming.sendFlower( ladybro );
// 路人将鲜花交给小姐姐的闺蜜,委托她在小姐姐心情好的时候将鲜花转交给小姐姐
passerby.sendFlower( ladybro );
使用代理模式中的虚拟代理
还是用上面书中的例子,鲜花的种类有很多种,每种鲜花的售价也不近相同,不同的鲜花也有不同的保质期。
小明为了夺得小姐姐的欢心,希望小姐姐的闺蜜在小姐姐心情好的时候,再去帮忙购买一束比较昂贵的鲜花转送给小姐姐,此时的操作就叫虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。
// 小姐姐的闺蜜
let ladybro = {
receiveFlower(flower){
if (flower.source === 'xiaoming') {
cuteGirl.listenGoodMood(() => { // 监听A的好心情
let flower = new Flower('xiaoming'); // 延迟创建flower 对象
cuteGirl.receiveFlower( flower );
});
} else {
throw new Error('小姐姐的闺蜜拒绝帮你送花!')
}
}
};
常见的虚拟代理实现
图片预加载
这里也是引用书中的例子,常见的开发需求之一,在图片未加载回来之前,希望有一个loading图进行占位,等loading图加载回来后再填充到img节点。
未使用代理模式
let MyImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;
img.onload = function(){
// 监听到图片加载完成后,设置src为加载完成后的图片
imgNode.src = img.src;
};
return {
setSrc: function( src ){
// 设置图片的时候,设置为默认的loading图
imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif';
// 把真正需要设置的图片传给Image对象的src属性
img.src = src;
}
}
})();
MyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
以上是未使用代理模式的写法,这也是常常容易写出来的代码情况,它在实现业务上并没有什么问题,但是MyImage
对象除了负责给img
节点设置src
外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则。我们在处理其中一个职责时,有可能因为其强耦合性影响另外一个职责的实现。
它同时还违反了开放—封闭原则,根据开放—封闭原则:
英文预加载loading的这个功能,是耦合进MyImage
对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在MyImage
对象里面改动代码。虽然MyImage
改动代码只需要几行就可以解决问题,但是换做其他甚至拥有10万行代码级别的JavaScript项目,要修改它的源代码风险就很大了。
使用代理模式
// 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口
let myImage = (function(){
let imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
//setSrc接口,外界调用这个接口,便可以给该img标签设置src属性
setSrc: function( src ){
imgNode.src = src;
}
}
})();
// 代理对象,负责图片预加载功能
let proxyImage = (function(){
// 创建一个Image对象,用于加载需要设置的图片
let img = new Image;
img.onload = function(){
// 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图片
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
// 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加载
myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'https://img.zcool.cn/community/01b620577ccc8b0000012e7ede064f.jpg@1280w_1l_2o_100sh.jpg' );
在使用了代理模式后:
图片本地对象负责往页面中创建一个img标签,并且提供一个对外的setSrc接口;
代理对象负责在图片未加载完成之前,引入预加载的loading图,负责了图片预加载的功能;
同时,它也满足了开放—封闭原则的基本思想:
我们并没有改变或者增加MyImage
的接口,但是通过代理对象,实际上给系统添加了新的行为(这里的行为是图片预加载)。
合并HTTP请求
这里也是引用一个书中的例子,例如我们需要做一个文件同步的功能,在选中对应的文件时,需要被同步到自己的OneDrive。
这里把OneDrive中的同步文件夹替换成网页中的checkbox
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
</body>
未使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
console.log( '开始同步文件,id为: ' + id );
};
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
// 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求
c.onclick = function(){
if ( this.checked === true ){
synchronousFile( this.id );
}
}
};
在未使用代理模式时,每选中一次checkbox,就会触发一次同步文件请求,频繁的网络请求,会给服务器带来比较大的开销,此时我们可以在不改变synchronousFile
函数职能的情况下,将它进行代理。
使用代理模式
// 同步文件请求的网络操作函数
let synchronousFile = function( id ){
console.log( '开始同步文件,id为: ' + id );
};
// 同步文件请求的网络操作函数-代理函数
let proxySynchronousFile = (function(){
let cache = [], // 保存一段时间内需要同步的ID
timer; // 定时器
return function( id ){
cache.push( id );
if ( timer ){ // 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function(){
synchronousFile( cache.join( ',' ) ); // 2秒后向本体发送需要同步的ID集合
clearTimeout( timer ); // 清空定时器
timer = null;
cache.length = 0; // 清空ID集合
}, 2000 );
}
})();
// 页面中所有的checkbox的选择器(因为上述的input的type只有checkbox,所以此时可以全部选中)
let checkbox = document.getElementsByTagName( 'input' );
// 遍历checkbox选择器
for ( let i = 0, c; c = checkbox[ i++ ]; ){
c.onclick = function(){
// 循环遍历添加点击事件,点击后如果是选中状态,则触发同步文件请求的代理函数
if ( this.checked === true ){
proxySynchronousFile( this.id );
}
}
};
synchronousFile
函数被代理后的函数我们起名为proxySynchronousFile
,它增加了一个缓存数组,所有两秒内的checkbox选中,都会被添加到缓存数组check中,等待2秒之后才把这2秒之内需要同步的文件ID一次性全打包发给服务器(将多个id拼接成逗号分割的字符串),在实时性要求不是很高的系统,这能大大减少服务器的压力。
惰性加载中的应用
来自于书中的例子,假设有一个迷你控制台的项目——miniConsole.js,它有一个log函数,专门用于打印参数。
// miniConsole.js代码
let miniConsole = {
log: function(){
// 真正代码略
console.log( Array.prototype.join.call( arguments ) );
}
};
export default miniConsole
因为这个控制台项目,是只在控制台展示的时候才需要的,我们希望他在有必要的时候才开始加载它,比如按F2的时候,加载miniConsole.js,就可以使用代理模式,惰性加载miniConsole.js。
大致的步骤是:
- 在用户敲击F2的时候,才去动态引入miniConsole.js的script标签
- 在用户敲击F2之前执行过的log命令,都会被缓存到代理对象内部的cache缓存数组内
- 等动态引入miniConsole.js的操作完成后,再从中逐一取出并执行。
详细代码如下:
// proxyMiniConsole.js代码
// miniConsole的代理对象
let proxyMiniConsole = (function(){
// 存储每次执行log时的回调函数
let cache = [];
let handler = function( ev ){
// 如果用户按了F2唤出了控制台
if ( ev.keyCode === 113 ){
// 执行引入miniConsole.js的操作
let script = document.createElement( 'script' );
script.src = 'miniConsole.js';
document.getElementsByTagName( 'head' )[0].appendChild( script );
document.body.removeEventListener( 'keydown', handler );// 只加载一次miniConsole.js
script.onload = function(){
// 如果miniConsole.js的script标签引入并加载完成
for ( var i = 0, fn; fn = cache[ i++ ]; ){
// 遍历所有缓存的回调函数并执行
fn();
}
};
}
};
// 监听键盘按键敲击事件
document.body.addEventListener( 'keydown', handler, false );
return {
// 返回代理后的方法
log: function(){
// 获取传入的所有参数
let args = arguments;
// 向缓存列表加入要打印的参数
cache.push( function(){
return miniConsole.log.apply( miniConsole, args );
});
}
}
})();
miniConsole.log( 11 ); // 开始打印log
使用代理模式中的缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
计算乘积
/**************** 计算乘积 *****************/
let mult = function(){
let a = 1;
for ( let i = 0, l = arguments.length; i < l; i++ ){
a = a * arguments[i];
}
return a;
};
/**************** 计算加和 *****************/
let plus = function(){
let a = 0;
for ( let i = 0, l = arguments.length; i < l; i++ ){
a = a + arguments[i];
}
return a;
};
/**************** 创建缓存代理的工厂 *****************/
let createProxyFactory = function( fn ){
// 缓存计算后的结果
let cache = {};
return function(){
// 通过字符串拼接所有传入的参数
let args = Array.prototype.join.call( arguments, ',' );
// 如果这个参数存在缓存内
if ( args in cache ){
// 则直接返回缓存的结果
return cache[args];
}
// 否则再对这个值进行计算
return cache[args] = fn.apply( this, arguments );
}
};
let proxyMult = createProxyFactory( mult ),
proxyPlus = createProxyFactory( plus );
console.log ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
console.log ( proxyMult( 1, 2, 3, 4 ) ); // 输出:24
console.log ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
console.log ( proxyPlus( 1, 2, 3, 4 ) ); // 输出:10
参考资料
[CUG-GZ]前端知识进阶——代理模式
www.yuque.com/cuggz/feplu…
前端设计模式之代理模式
juejin.cn/post/684490…
漫画:什么是 “代理模式” ?
mp.weixin.qq.com/s/O8_A2Ms9M…
JavaScript设计模式与开发实践
www.ituring.com.cn/book/1632
从ES6重新认识JavaScript设计模式(五): 代理模式和Proxy
segmentfault.com/a/119000001…
使用 JavaScript 原生的 Proxy 优化应用
juejin.cn/post/684490…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!