前言
这是关于设计模式的系列文章,在每篇文章中将对常见设计模式进行讲解,因为针对前端方向,而且前端常用语言
JavaScript
本身是弱类型,面向对象(模拟面向对象)编程的实现相较于其他强类型语言实现更为繁琐,所以代码主要以JavaScript
表现。
系列文章链接:
代理模式概念
由于某些情况下一个对象不能直接引用另一个对象,所以需要代理对象在这两个对象之间起到中介作用或者实现控制,这样的模式叫 “代理模式”。
基本实现
// 假设无法客户端无法直接使用这个类
class Google {
get(url) {
return url + ' is google';
}
}
// 只能通过代理操作 Google 类
class Proxy {
constructor() {
this.google = new Google();
}
get(url) {
return this.google.get(url);
}
}
const proxy = new Proxy();
const result = proxy.get('http://www.google.com');
console.log(result); // http://www.google.com is google
假设 Google
类我们无法直接使用,只有 Proxy
可以使用 Google
,我们可以通过 Proxy
类去操作使用 Google
类,此时 Proxy
类就是一个代理。
ES6 的 Proxy
在 ES6
标准以后,JavaScript
提供了原生的代理模式 Proxy
类,可以代理其他对象,并在对象属性的获取和赋值时增加拦截。
/* ES6 Proxy 的使用 */
const lucy = {
name: 'lucy',
age: 20,
height: 165
};
const lucyMother = new Proxy(lucy, {
get(target, key) {
if (key === 'age') {
return target.age - 2;
} else if (key === 'height') {
return target.height + 5;
} else {
return target[key];
}
},
set(target, key, val) {
if (key === 'boyfriend') {
if (val.age > 40) {
console.log('太老了');
} else if (val.salary < 20000) {
console.log('太穷了');
} else {
target[key] = val;
}
}
}
});
console.log(lucyMother.name); // lucy
console.log(lucyMother.age); // 18
console.log(lucyMother.height); // 170
lucyMother.boyfriend = {
age: 42,
salary: 25000
}
// 太老了
lucyMother.boyfriend = {
age: 36,
salary: 18000
}
// 太穷了
上面是一个接地气的案例,创建一个对象存储 lucy
的基本信息,使用代理创建 lucyMother
为 lucy
找男朋友,通过代理对象获取 lucy
的基本信息时会虚报年龄和身高,而在设置男朋友对象时会检查是否符合要求。
代理模式、适配器模式和装饰器模式
从代码实现来看,代理模式、适配器模式、装饰器模式非常的相似,非常容易混淆,但其实是有本质区别的。
- 代理模式和适配器模式:代理模式不会改变原有的接口,代理类和被代理的类属性方法使用方式完全一致,而适配器模式是因为旧的接口无法使用,通过适配器创建新的接口去兼容旧的接口;
- 代理模式和装饰器模式:装饰器功能会保证被装饰类功能正常使用的情况下新增功能,而代理模式保证原有接口,但会改变原来接口的功能;
- 适配器模式和装饰器模式:装饰器是对一个类的包装,而适配器更多是去建立提供接口的类与无法适配的类之间的联系。
代理模式的应用
事件委托
事件委托是浏览器事件注册的一种优化手段,如果同类型的元素非常多,且都有相同的事件,如列表,则不必给每一个元素注册这个事件,而是将事件注册给父元素,即将事件委托给父元素,避免了相同事件的重复注册,这种优化利用了 “代理模式”,又称事件代理。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件委托</title>
</head>
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
const ulList = document.getElementById('list');
ulList.addEventListener('click', function (event) {
console.log(event.target.innerHTML);
});
</script>
</body>
</html>
在浏览器中,委托给父元素的事件触发后,可以通过事件对象的属性 target
获取到具体触发事件的子元素。
图片加载
图片加载是一个提高用户体验的功能,也是非常常见的,原因是浏览器向服务器请求资源图片是需要等待的,由于网络等因素的影响会导致等待的时间更长,此时我们需要一个 loading
图片来过渡,这就是图片加载的基本需求。
/* node 服务器 */
const express = require('express');
const path = require('path');
const app = express();
app.get('/loading.gif', function (req, res) {
res.sendFile(path.resolve('img', 'loading.gif'));
});
app.get('/img/:name', function (req, res) {
setTimeout(function () {
res.sendFile(path.join(__dirname, req.path));
}, 3000);
});
app.use(express.static(__dirname));
app.listen(3000, function () {
console.log('server start 3000');
});
上面服务器模拟了加载图片响应慢的场景,loading
图片立即响应,其他图片则延迟 3s
响应。
<!-- Dom 结构 -->
<ul id="menu">
<li data-src="/img/bg1.jpg">图片1</li>
<li data-src="/img/bg2.jpg">图片2</li>
</ul>
<div id="bgimg"></div>
/* 没有实现 loading */
const menu = document.getElementById('menu');
const bgimg = document.getElementById('bgimg');
const background = (function () {
const img = new Image();
bgimg.appendChild(img)
return {
setSrc(src) {
img.src = src;
}
}
})();
menu.addEventListener('click', function (event) {
const src = event.target.dataset.src;
background.setSrc(src);
});
上面的代码是没有实现懒加载的,当点击按钮向服务器请求图片时,并没有加入 loading
图片过渡,之所以说图片加载应用了 “代理模式” 并不是指加载功能本身,而是我们的实现方式,编写的代码质量要高至少要遵循单一职责原则和开放封闭原则,就是说最好不要直接在事件监听的函数中增加 loading
过渡的逻辑,而是把这个过渡功能交给代理对象去处理。
/* 使用代理对象实现 loading 过渡 */
const menu = document.getElementById('menu');
const bgimg = document.getElementById('bgimg');
// 请求图片的对象
const background = (function () {
const img = new Image();
bgimg.appendChild(img)
return {
setSrc(src) {
img.src = src;
}
}
})();
// 增加 loading 过度的代理对象
const proxyBackground = (function () {
const img = new Image();
img.onload = function () {
background.setSrc(this.src);
}
return {
setSrc(src) {
background.setSrc('./img/loading.gif');
img.src = src;
}
}
})();
// 监听获取图片的事件中使用的是代理对象 proxyBackground
menu.addEventListener('click', function (event) {
const src = event.target.dataset.src;
// 防止缓存
proxyBackground.setSrc(src + '?time=' + Date.now());
});
上面的实现方式就符合 “代理模式”,background
对象是提供基本功能,而proxyBackground
(代理对象)增强了基本功能,却并没有改变接口的使用方式,依然通过 setSrc
方法去请求图片。
防抖代理
防抖的作用是在做一个操作时不需要很频繁,如搜索查询,在连续输入时如果每次触发输入事件都向后端发送请求,性能是极差的,我们希望的是连续输入只在最后一次统一发送请求,这种处理叫做防抖处理,是前端优化的手段。
<!-- 未使用防抖代理处理 -->
<input type="text" id="ipt">
<script>
const ipt = document.getElementById('ipt');
function post() {
console.log('发送请求了');
}
ipt.addEventListener('input', post);
</script>
上面代码未使用防抖代理,每次输入都会打印 “发送请求了”。
<!-- 使用防抖代理优化 -->
<input type="text" id="ipt">
<script>
const ipt = document.getElementById('ipt');
function post() {
console.log('发送请求了');
}
// 代理函数去执行 post
const debouncePost = (function () {
let timer = null;
return function () {
clearInterval(timer);
timer = setTimeout(function () {
post();
}, 500);
}
})();
ipt.addEventListener('input', debouncePost);
</script>
使用防抖代理函数优化后,保留了原有功能的基础上进行了增强,实现了连续输入停止 500ms
后统一发送一次请求,防抖的实现方式有很多种,包括并不限于函数式编程等,而上面代码使用了 “代理模式” 实现 。
总结
使用 “代理模式” 的场景在后端会更多,比如代理跨域,
Nginx
代理等等,还有一点需要注意的是,“代理模式” 并非单一的,对于同一个对象,可以有多个代理对象去增强不同的功能,最后附上 案例地址。