javascript 设计模式之代理模式

779 阅读4分钟

文章系列

javascript 设计模式之单例模式

javascript 设计模式之适配器模式

javascript 设计模式之装饰者模式

javascript设计模式之代理模式

javascript 适配、代理、装饰者模式的比较

javascript 设计模式之状态模式

javascript 设计模式之迭代器模式

javascript 设计模式之策略模式

javascript 设计模式之观察者模式

javascript 设计模式之发布订阅者模式

概念

代理模式:使用者无权访问目标对象,中间加代理,通过代理做授权和控制

本文代码

为什么需要代理

科学上网

大家都知道中国大陆是无法访问谷歌的,一访问会报 无法访问 这是由于国内限制了Google.com 对应的 IP 地址。 这时你就要通过 VPN 之后即可正常访问,所谓的 VPN 实际上就是做了一层代理服务器,使得 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'

当你要访问吴京的名字跟年龄时,可以直接访问(男明星的年龄一般不是秘密)。

当你要获取吴京的号码,那只能给你经纪人的号码。

当你要问出场价时,由经纪人这边定价。

另外当你要砍价时,也是由经纪人出面谈。

开发建议

在开发时候不要先去猜测是否需要使用代理模式, 如果发现直接使用某个对象不方便时, 再来优化不迟。