Skip to content
On this page

代理模式

科学上网

“翻墙”就是典型的代理模式,普通的访问 image.png 如果需要“翻墙”则需要在中间加一层代理服务器 image.png

代理器 proxy

ES6 中的代理器

javascript
const proxy = new Proxy(obj, handler)

当我们通过 proxy 去访问目标对象的时候,handler会对我们的行为作一层拦截,我们的每次访问都需要经过 handler 这个第三方。

定义代理对象

javascript
// 未知妹子
const girl = {
  // 姓名
  name: '小美',
  // 自我介绍
  aboutMe: '...'(大家自行脑补吧)
  // 年龄
  age: 24,
  // 职业
  career: 'teacher',
  // 假头像
  fakeAvatar: 'xxxx'(新垣结衣的图片地址)
  // 真实头像
  avatar: 'xxxx'(自己的照片地址),
  // 手机号
  phone: 123456,
}

访问拦截

只有在登录充值 VIP 才能访问部分真实信息

javascript
// 普通私密信息
const baseInfo = ['age', 'career']
// 最私密信息
const privateInfo = ['avatar', 'phone']

// 用户(同事A)对象实例
const user = {
    ...(一些必要的个人信息)
    isValidated: true,
    isVIP: false,
}

const JuejinLovers = new Proxy(girl, {
  get: function(girl, key) {
      if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
          alert('您还没有完成验证哦')
          return
      }
      
      //...(此处省略其它有的没的各种校验逻辑)
    
      // 此处我们认为只有验证过的用户才可以购买VIP
      if(user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
          alert('只有VIP才可以查看该信息哦')
          return
      }
  }
})

设置拦截

以上主要是 getter 层面的拦截。假设我们还允许会员间互送礼物,每个会员可以告知婚介所自己愿意接受的礼物的价格下限,我们还可以作 setter 层面的拦截

javascript
// 规定礼物的数据结构由type和value组成
const present = {
    type: '巧克力',
    value: 60,
}

// 为用户增开presents字段存储礼物
const girl = {
  // 姓名
  name: '小美',
  // 自我介绍
  aboutMe: '...'(大家自行脑补吧)
  // 年龄
  age: 24,
  // 职业
  career: 'teacher',
  // 假头像
  fakeAvatar: 'xxxx'(新垣结衣的图片地址)
  // 真实头像
  avatar: 'xxxx'(自己的照片地址),
  // 手机号
  phone: 123456,
  // 礼物数组
  presents: [],
  // 拒收50块以下的礼物
  bottomValue: 50,
  // 记录最近一次收到的礼物
  lastPresent: present,
}

// 小礼物功能
const JuejinLovers = new Proxy(girl, {
  get: function(girl, key) {
    if(baseInfo.indexOf(key)!==-1 && !user.isValidated) {
        alert('您还没有完成验证哦')
        return
    }
    
    //...(此处省略其它有的没的各种校验逻辑)
  
    // 此处我们认为只有验证过的用户才可以购买VIP
    if(user.isValidated && privateInfo.indexOf(key)!==-1 && !user.isVIP) {
        alert('只有VIP才可以查看该信息哦')
        return
    }
  }
  
  set: function(girl, key, val) {
 
    // 最近一次送来的礼物会尝试赋值给lastPresent字段
    if(key === 'lastPresent') {
      if(val.value < girl.bottomValue) {
          alert('sorry,您的礼物被拒收了')
          return
      }
    
      // 如果没有拒收,则赋值成功,同时并入presents数组
      girl.lastPresent = val
      girl.presents = [...girl.presents, val]
    }
  }
 
})

事件代理

给多个 a 标签添加事件

javascript
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>事件代理</title>
</head>

<body>
  <div id="father">
    <a href="#">链接1号</a>
    <a href="#">链接2号</a>
    <a href="#">链接3号</a>
    <a href="#">链接4号</a>
    <a href="#">链接5号</a>
    <a href="#">链接6号</a>
  </div>
</body>

</html>

首先可能会直接考虑,循环给不同的 a 标签绑定不同的事件

javascript
const aNodes = document.getElementById('father').getElementsByTagName('a')

const aLength = aNodes.length

for (let i = 0; i < aLength; i++) {
  aNodes[i].addEventListener('click', function (e) {
    e.preventDefault()
    alert(`我是${aNodes[i].innerText}`)
  })
}

这种绑定如果标签不断的增多,性能开销会逐步增大,如果考虑到点击标签事件会进行“冒泡”,点击事件会“冒泡”到父元素 div 上,从而被监听到。如此一来,点击事件的监听函数只需要在 div 元素上被绑定一次即可,而不需要在子元素上被绑定 N 次——这种做法就是事件代理,它可以很大程度上提高我们代码的性能。

javascript
// 获取父元素
const father = document.getElementById('father')

// 给父元素安装一次监听函数
father.addEventListener('click', function (e) {
  // 识别是否是目标子元素
  if (e.target.tagName === 'A') {
    // 以下是监听函数的函数体
    e.preventDefault()
    alert(`我是${e.target.innerText}`)
  }
})

点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。

虚拟代理

图片预加载

javascript
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>虚拟代理</title>
</head>

<body>
  <div class="imgs">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
    <img src="" alt="" data-src="https://imondo.cn/files/logo.png">
  </div>
  <script>
    class PreloadImage {
      constructor(imgNode) {
        // 获取真实的DOM节点
        this.imgNode = imgNode
      }

      // 操作img节点的src属性
      setSrc(imgUrl) {
        this.imgNode.src = imgUrl
      }
    }

    class ProxyImage {
      // 占位图的url地址
      static LOADING_URL = 'timer.png'

      constructor(targetImage) {
        // 目标Image,即PreLoadImage实例
        this.targetImage = targetImage
      }

      // 该方法主要操作虚拟Image,完成加载
      setSrc(targetUrl) {
        // 真实img节点初始化时展示的是一个占位图
        this.targetImage.setSrc(ProxyImage.LOADING_URL)
        // 创建一个帮我们加载图片的虚拟Image实例
        const virtualImage = new Image()
        // 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
        virtualImage.onload = () => {
          this.targetImage.setSrc(targetUrl)
        }
        // 设置src属性,虚拟Image实例开始加载图片
        virtualImage.src = targetUrl
      }
    }
    Array.prototype.forEach.call(document.querySelectorAll('img'), function (imgNode) {
      new ProxyImage(new PreloadImage(imgNode)).setSrc(imgNode.getAttribute('data-src'))
    })
  </script>
</body>

</html>

缓存代理

空间换时间。例如计算过的不重复计算

javascript
const addAll = function () {
  console.log('计算了一次');
  let result = 0;
  const len = arguments.length;
  for (let i = 0; i < len; i++) {
    result += arguments[i];
  }
  return result;
}

const proxyAddAll = (function () {
  // 求和结果的缓存池
  const resultCache = {};

  return function () {
    // 将入参转化为一个唯一的入参字符串
    const args = Array.prototype.join.call(arguments, ',');

    // 检查本次入参是否有对应的计算结果
    if (args in resultCache) {
      // 如果有,则返回缓存池里现成的结果
      return resultCache[args]
    }
    return resultCache[args] = addAll(...arguments)
  }
})()

image.png 代理模式的套路就只有一个—— A 不能直接访问 B,A 需要借助一个帮手来访问 B,这个帮手就是代理器。