代理模式:代理实现图片预加载

888 阅读5分钟

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。 ​

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 ​

虚拟代理

在Web开发中,图片预加载是一种常用的技术,如果直接给某个img标签节点设置src属性,由于图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。常见的做法是先用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img节点里,这种场景就很适合使用虚拟代理。下面我们来实现这个虚拟代理:

class PreLoadImage {
  constructor(imgNode) {
    // 获取真实的DOM节点
    this.imgNode = imgNode
  }

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

class ProxyImage {
  // 占位图的url地址
  static LOADING_URL = 'xxxxxx'

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
	}
}

ProxyImage 帮我们调度了预加载相关的工作,我们可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问,并得到我们想要的效果。 ​

代理和本体接口的一致性

如果有一天我们不再需要预加载,那么就不再需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理对象和本体是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这样做有两个好处。

  • 用户可以放心地请求代理,他只关心是否能得到想要的结果。
  • 在任何使用本体的地方都可以替换成使用代理。

缓存代理

缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。

一个比较典型的例子,是对传入的参数进行求和:

// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
  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)
  }
})()

我们发现 proxyAddAll 针对重复的入参只会计算一次,这将大大节省计算过程中的时间开销。现在我们有 6 个入参,可能还看不出来,当我们针对大量入参、做反复计算时,缓存代理的优势将得到更充分的凸显。 ​

事件代理

事件代理(Event Delegation),又称之为事件委托。是JavaScript中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定在子元素的响应事件(click、keydown......)委托给父元素,让父元素担当事件监听的职务。它可以很大程度上提高我们代码的性能。事件代理的原理是DOM元素的事件冒泡。

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

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

其他代理模式

  • 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在Java中,远程代理可以是另一个虚拟机中的对象。
  • 保护代理:用于对象应该有不同访问权限的情况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是其典型运用场景。

总结

理模式包括许多小分类,在JavaScript开发中最常用的是虚拟代、理缓存代理和事件代理。虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。