概念
代理模式:使用者无权访问目标对象,中间加代理,通过代理做授权和控制
为什么需要代理
科学上网,俗称翻墙
大家都知道中国大陆是无法访问谷歌的,一访问会报
这是由于国内限制了Google.com 对应的 IP 地址。
这时你就要通过翻墙之后即可正常访问,所谓的翻墙实际上就是做了一层代理服务器,使得 DNS 解析之后不是直接达到目标服务器,而是到代理服务器,代理服务器对应的 IP 可不在限制 IP 之列,所以可以正常访问。再由代理服务器向目标服务器发起请求,目标服务器将请求返回内容也返回给代理服务器,再由代理服务器返回给用户。
明星和经纪人的事
某个大商场要开业,要请明星来撑台面,可是商场负责人并不知道明星的电话。
确实明星这么脱俗的职业,哪能直接谈钱,另外如果将号码公布出来,不得经常被粉丝打扰,所以就需要经纪人出面谈钱事宜。
这里的经纪人就是作为明星的代理人
代理类型
事件代理
<div id="div1">
<a href="#">a1</a>
<a href="#">a2</a>
<a href="#">a3</a>
<a href="#">a4</a>
<a href="#">a5</a>
</div>
复制代码
需求:
点击每个 a 标签,都可以弹出对应 a 标签文本内容
代码实现:
如果不用事件代理,需要循环在每个 a 标签上绑定事件,代码如下:
var aTags = document.getElementById('div1').getElementsByTagName('a')
for(let i=0;i<aTags.length;i++) {
aTags[i].addEventListener('click', function(e) {
e.preventDefault()
alert(aNodes[i].innerText)
})
}
复制代码
如果 a 标签的数量增多,性能开销是很大的。
根据事件冒泡原理,可以在父元素上绑定一次事件即可。代码改成如下:
// 获取父元素
var div1 = document.getElementById('div1')
// 给父元素安装一次监听函数
div1.addEventListener('click', function (e) {
var target = e.target
// 识别是否是目标子元素
if (target.nodeName == 'A') {
e.preventDefault()
alert(target.innerText)
}
})
复制代码
点击操作不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
虚拟代理
需求:
首屏有张图片过大,直接显示会有一段时间留白,体验不好。所以打算采用预加载,何谓预加载呢?
实际上就是先让这个 img 标签展示一个占位图,然后创建 Image 实例,将 src 设置为真实的图片地址,当真实图片完全加载好后( onload ),给该 Dom 元素对应的 img 元素设置为真实的图片地址,由于图片地址已经加载好了,已经有了该图片的缓存内容,所以展示速度会非常快,提升了用户体验。
代码实现:
class PreloadImage {
constructor(imageNode){
// 获取真实的DOM节点
this.imageNode = imageNode
}
// 操作img节点的src属性
setSrc(url){
this.imageNode.src = url
}
}
class ProxyImage {
constructor(targetImage) {
// 目标Image,即PreLoadImage实例
this.targetImage = targetImage
// 占位图的url地址
this.loadingUrl = 'xxx';
}
// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl) {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(this.loadingUrl)
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image()
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
// this.targetImage.setSrc(targetUrl)
}
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl
}
}
const imgNode = document.createElement('img')
document.body.appendChild(imgNode)
const preloadImg = new PreloadImage(imgNode)
const proxyImg = new ProxyImage(preloadImg)
proxyImg.setSrc("https://www.google.com.hk/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png")
复制代码
示例中,virtualImage 这个对象是一个“幕后英雄”,它始终存在于 JavaScript 世界中、代替真实 DOM 发起了图片加载请求、完成了图片加载工作,却从未在渲染层面抛头露面。因此这种模式被称为“虚拟代理”模式。
缓存代理
何谓缓存代理?
用在进行大量运算时,通过将计算结果缓存下来,而不用每次都进行计算,来提升效率。
比如实现乘积计算:
// 将传入的所有参数相乘
const mult = function() {
console.info("执行了一次计算")
let a = 1
for (let i = 0, l; l = arguments[i++];) {
a = a * l
}
return a
}
// 为上面的乘积方法创建代理
const proxyMult = (function() {
// 乘积结果的缓存池
const cache = {}
return function() {
// 将入参转化为一个唯一的入参字符串,作为是否执行过运算的标识符
const tag = Array.prototype.join.call(arguments, ',')
// 执行过,则直接返回
if (cache[tag]) {
// 如果执行过,则返回缓存池里现成的结果
return cache[tag]
}
cache[tag] = mult.apply(this, arguments)
return cache[tag]
}
})()
console.info(proxyMult(1, 2, 3, 4))// 24
console.info(proxyMult(1, 2, 3, 4))// 24
复制代码
可以发现 mult 针对重复的入参只会计算一次,只有四个参数时还看不出有啥性能提升,但参数过多,运算够复杂时,缓存代理的作用就体现出来了。
保护代理
何谓保护代理?
实际上上面提到的明星与经纪人就是典型的保护代理,经纪人就是起到”保护”明星的作用。
在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。
代码实现:
const star = {
name:'吴京',
age:48,
phone:'star:13544030212',
price:'12000'
}
const agent = new Proxy(star,{
get:function(target,key){
if(key == 'phone'){
return 'agent:354321';
}
if(key == 'price'){
return '15000'
}
return star[key]
},
set:function(target,key,val){
if(key == 'customePrice'){
if(val<1000){
throw new Error('价格过低')
}else{
target[key] = val;
return true;
}
}
}
})
console.info(agent.name)
console.info(agent.age)
console.info(agent.phone)
console.info(agent.price)
agent.customePrice = '20000';
console.info(agent.customePrice);
agent.customePrice = '200'
复制代码
当你要访问吴京的名字跟年龄时,可以直接访问(男明星的年龄一般不是秘密)。
当你要获取吴京的号码,那只能给你经纪人的号码。
当你要问出场价时,由经纪人这边定价。
另外当你要砍价时,也是由经纪人出面谈。
开发建议
在开发时候不要先去猜测是否需要使用代理模式, 如果发现直接使用某个对象不方便时, 再来优化不迟。