虚拟代理

使用虚拟代理实现延迟加载和性能优化

问题

什么是虚拟代理?如何在前端中应用虚拟代理模式?

解答

虚拟代理是代理模式的一种,用于延迟创建开销大的对象,直到真正需要时才创建。代理对象控制对真实对象的访问,在访问前可以执行额外操作。

图片懒加载

最常见的虚拟代理应用:

// 真实图片加载器
class RealImage {
  constructor(container, src) {
    this.img = document.createElement('img')
    this.img.src = src
    container.appendChild(this.img)
  }
}

// 虚拟代理:先显示占位图,加载完成后替换
class ProxyImage {
  constructor(container, src) {
    this.container = container
    this.src = src
    this.realImage = null
    
    // 先显示占位图
    this.placeholder = document.createElement('img')
    this.placeholder.src = 'loading.gif'
    container.appendChild(this.placeholder)
    
    // 后台加载真实图片
    this.loadImage()
  }
  
  loadImage() {
    const img = new Image()
    img.onload = () => {
      // 加载完成,替换占位图
      this.placeholder.src = this.src
    }
    img.src = this.src
  }
}

// 使用
const container = document.getElementById('image-container')
new ProxyImage(container, 'large-image.jpg')

延迟初始化

对于创建成本高的对象,延迟到首次使用时才初始化:

// 假设这是一个初始化很慢的编辑器
class HeavyEditor {
  constructor() {
    console.log('初始化编辑器...耗时操作')
    this.content = ''
  }
  
  setContent(text) {
    this.content = text
  }
  
  getContent() {
    return this.content
  }
}

// 虚拟代理:延迟创建
class EditorProxy {
  constructor() {
    this.editor = null // 延迟创建
  }
  
  // 获取真实对象,不存在则创建
  getEditor() {
    if (!this.editor) {
      this.editor = new HeavyEditor()
    }
    return this.editor
  }
  
  setContent(text) {
    this.getEditor().setContent(text)
  }
  
  getContent() {
    return this.getEditor().getContent()
  }
}

// 使用
const editor = new EditorProxy() // 此时不会初始化 HeavyEditor
console.log('代理已创建')

// 首次调用时才初始化
editor.setContent('Hello') // 这里才会初始化 HeavyEditor

使用 Proxy 实现

ES6 Proxy 可以更优雅地实现虚拟代理:

function createLazyProxy(factory) {
  let instance = null
  
  return new Proxy({}, {
    get(target, prop) {
      // 首次访问时创建实例
      if (!instance) {
        instance = factory()
      }
      
      const value = instance[prop]
      // 如果是方法,绑定 this
      return typeof value === 'function' 
        ? value.bind(instance) 
        : value
    }
  })
}

// 使用
const lazyEditor = createLazyProxy(() => {
  console.log('创建编辑器实例')
  return new HeavyEditor()
})

console.log('代理已创建,实例未创建')
lazyEditor.setContent('test') // 此时才创建实例

合并请求

虚拟代理还可用于合并短时间内的多次请求:

// 合并文件同步请求
class SyncProxy {
  constructor(syncFn) {
    this.syncFn = syncFn
    this.cache = new Set()
    this.timer = null
  }
  
  sync(fileId) {
    this.cache.add(fileId)
    
    // 清除之前的定时器
    if (this.timer) {
      clearTimeout(this.timer)
    }
    
    // 延迟执行,合并请求
    this.timer = setTimeout(() => {
      this.syncFn([...this.cache])
      this.cache.clear()
    }, 2000)
  }
}

// 使用
const syncProxy = new SyncProxy((ids) => {
  console.log('批量同步文件:', ids)
})

// 多次调用会被合并
syncProxy.sync('file1')
syncProxy.sync('file2')
syncProxy.sync('file3')
// 2秒后只发送一次请求:['file1', 'file2', 'file3']

关键点

  • 虚拟代理延迟创建开销大的对象,直到真正需要时才实例化
  • 图片懒加载是最典型的应用场景
  • ES6 Proxy 可以透明地拦截对象访问,实现更优雅的虚拟代理
  • 虚拟代理还可用于合并请求、缓存结果等场景
  • 代理对象与真实对象接口一致,对调用者透明