9_享元模式

1 初识享元模式

  • 用于性能优化的模式

运用共享技术来有效支持大量细粒度的对象

获取内衣广告图片

内衣工厂,目前的产品有50种男士内衣和50种女士内衣,各有50个模特做展示拍照片

class Modal {
  constructor(sex, underwear) {
    this.sex = sex
    this.underwear = underwear
  }
  takePhoto() {
    console.log(
      `sex=${this.sex} underwear=${this.underwear}`
    )
  }
}
for(let i = 1; i <= 50; i++) {
  let maleModal = new Modal('male', 'underwear' + i)
  maleModal.takePhoto()
}
for(let i = 1; i <= 50; i++) {
  let femaleModal = new Modal('female', 'underwear' + i)
  femaleModal.takePhoto()
}

此时产生了100个对象

男女模特各一个,分别穿上不同的内衣来拍照

class Modal {
  constructor(sex) {
    this.sex = sex
    this.underwear
  }
  setUnderwear(underwear) {
    this.underwear = underwear
  }
  takePhoto() {
    console.log(
      `sex=${this.sex} underwear=${this.underwear}`
    )
  }
}
let maleModal = new Modal('male'),
  femaleModal = new Modal('female')

for(let i = 1; i <= 50; i++) {
  maleModal.setUnderwear('underwear' + i)
  maleModal.takePhoto()
}
for(let i = 1; i <= 50; i++) {
  femaleModal.setUnderwear('underwear' + i)
  femaleModal.takePhoto()
}

2 内部状态与外部状态

目标:尽量减少共享对象的数量

用时间换空间

  • 如何划分内部状态与外部状态
内部状态 外部状态
存储于对象内部
可以被一些对象共享 不能被共享
独立于具体场景,通常不会改变 取决于具体场景,并根据场景而改变

3 享元模式的通用结构

存在的问题
  1. 惰性创建
  2. 外部状态可能相当复杂,它们与共享对象的联系会变得困难

    使用管理器记录对象相关的外部状态,使得外部状态通过某个钩子和共享对象联系起来

4 文件上传的例子

微云上传模块开发

4.1 对象爆炸

1. 文件上传
let id = 0
window.startUpload = function(uploadType, files) {
  for(let file in files) {
    let uploadObj = new Upload(
      uploadType,
      file.fileName,
      file.fileSize
    )
    // 为upload对象设置唯一的id
    uploadObj.init(id++)
  }
}
2. Upload 类
class Upload {
  constructor(uploadType, fileName, fileSize) {
    this.uploadType = uploadType
    this.fileName = fileName
    this.fileSize = fileSize
    this.id = null
    this.dom = null
  }
  init(id) {
    this.id = id
    this.dom = document.createElement('div')
    this.dom.innerHTML = `
      <span>文件名:${this.fileName},
      文件大小:${this.fileSize}</span>
      <button class="delFile">delete</button>
    `
    this.dom.querySelector('.delFile').onclick = () => {
      this.delFile()
    }
    document.body.appendChild(this.dom)
  }
  delFile() {
    let flage = window.confirm(
      '确定删除该文件吗?' + this.fileName
    )
    if(flage) {
      return this.dom.parentNode.removeChild(this.dom)
    }
  }
}
3. 测试
startUpload('plugin', [{
  fileName: '1.txt', fileSize: 1000
}, {
  fileName: '2.txt', fileSize: 2000
}, {
  fileName: '3.txt', fileSize: 3000
}])
startUpload('flash', [{
  fileName: '4.txt', fileSize: 4000
}, {
  fileName: '5.txt', fileSize: 5000
}, {
  fileName: '6.txt', fileSize: 6000
}])

4.2 享元模式重构文件上传

  • uploadType 是内部状态
    • 不同上传方式的实际工作原理有很大区别

      各自调用的接口需在对象创建之初便明确其类型

    • 上传方式可以被任何文件共用
  • fileName fileSize 是外部状态
    • 根据场景而变化

4.3 剥离外部状态

外部状态--文件名和大小存储在外部管理器 uploadManager

将upload对象初始化工作提取到 uploadManager.add 函数 -- 不再需要init函数

定义delFile函数

删除文件之前需要读取文件的实际大小,通过 uploadManager.setExternalState() 给共享对象设置正确的 fileSize

class Upload {
  constructor(uploadType) {
    this.uploadType = uploadType
  }
  delFile(id) {
    // 把当前id对应的对象的外部状态都组装到共享对象中
    uploadManager.setExternalState(id, this)
    let flage = window.confirm(
      '确定删除该文件吗?' + this.fileName
    )
    if(flage) {
      return this.dom.parentNode.removeChild(this.dom)
    }
  }
}

4.4 创建upload对象的工厂函数

如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象

class UploadFactory {
  constructor() {
    this.createdFlyWeightObjs = {}
  }
  create(uploadType) {
    if(this.createdFlyWeightObjs[uploadType]) {
      return this.createdFlyWeightObjs[uploadType]
    }
    return this.createdFlyWeightObjs[uploadType]
      = new Upload(uploadType)
  }
}

4.5 管理器封装外部状态

uploadManager 对象:

  • 负责向 UploadFactory 提交创建对象的请求
  • 并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态,以便在程序运行过程中给 upload 共享对象设置外部状态
class UploadManager {
  constructor() {
    this.uploadDatabase = {}
    this.uploaFatory = new UploadFactory()
  }
  add(id, uploadType, fileName, fileSize) {
    let flyWeightObj = this.uploaFatory.create(uploadType)
    let dom = document.createElement('div')
    dom.innerHTML = `
      <span>文件名:${fileName},
      文件大小:${fileSize}</span>
      <button class="delFile">delete</button>
    `
    dom.querySelector('.delFile').onclick = () => {
      flyWeightObj.delFile(id)
    }
    document.body.appendChild(dom)
    this.uploadDatabase[id] = {
      fileName,
      fileSize,
      dom
    }
    return flyWeightObj
  }
  setExternalState(id, flyWeightObj) {
    let uploadData = this.uploadDatabase[id]
    for(let i in uploadData) {
      flyWeightObj[i] = uploadData[i]
    }
  }
}

4.6 应用 -- 定义触发上传动作的 startUpload 函数

let id = 0
let uploadManager = new UploadManager()
window.startUpload = (uploadType, files) => {
  for(let file in files) {
    let uploadObj = uploadManager.add(
      ++id, uploadType, file.fileName, file.fileSize
    )
  }
}

4.7 测试

startUpload('plugin', [{
  fileName: '1.txt', fileSize: 1000
}, {
  fileName: '2.txt', fileSize: 2000
}, {
  fileName: '3.txt', fileSize: 3000
}])
startUpload('flash', [{
  fileName: '4.txt', fileSize: 4000
}, {
  fileName: '5.txt', fileSize: 5000
}, {
  fileName: '6.txt', fileSize: 6000
}])

5 享元模式的适用性

  1. 一个程序中应用了大量的相似对象
  2. 由于使用了大量对象,造成了很大的内存开销
  3. 对象的大多数状态都可以变为外部对象
  4. 剥离出对象的外部状态之后可以用相对较少的共享对象取代大量对象

6 再谈内部状态和外部状态

6.1 没有内部状态的享元

网站不需要考虑极速上传和普通上传之间的切换

1. 不区分 uploadType
class Upload {
  constructor() {}
}
2. 创建享元对象的工厂
class UploadFactory {
  construtor() {
    this.uploadObj = {}
  }
  create() {
    if(this.uploadObj) {
      return this.uploadObj
    }
    return this.uploadObj = new Upload()
  }
}
3. 管理器部分不变
  • 当对象没有内部状态时,生产共享对象的工厂实际上变成了一个单例工厂,但还是有剥离外部状态的过程,依旧称之为享元模式

6.2 没有外部状态的享元

  • 当对象没有外部状态时,没有剥离外部状态的过程,即使用到了共享技术,但并不是一个享元模式

7 对象池

对象池维护一个装载空闲对象的池子

  • 需要对象时,不是直接 new,而是从对象池里获取
  • 如果对象池里没有空闲对象,则创建一个新的对象

7.1 对象池实现

1. 获取小气泡节点的工厂
class ToolTipFactory {
  constructor() {
    // 对象池
    this.toolTipPool = []
  }
  // 获取一个div节点
  create() {
    if(!this.toolTipPool.length) {
      let div = document.createElement('div')
      document.body.appendChild(div)
      return div
    } else {
      return this.toolTipPool.shift()
    }
  }
  // 回收一个div节点
  recover(toolTipDom) {
    return this.toolTipPool.push(toolTipDom)
  }
}
2. 第一次搜索获得2个小气泡节点
let arr = []
let toolTipFactory = new ToolTipFactory()
for(let i = 0, str; str = ['A', 'B'][i++]; ) {
  let toolTip = toolTipFactory.create()
  toolTip.innerHTML = str
  arr.push(toolTip)
}
3. 将第一次搜索获得的小气泡收入对象池
for(let toolTip in arr) {
  toolTipFactory.recover(toolTip)
}
4. 二次搜索获得6个小气泡节点
for(let i = 0, str; str = ['A', 'B', 'C', 'D', 'E', 'F'][i++]; ) {
  let toolTip = toolTipFactory.create()
  toolTip.innerHTML = str
}

7.2 通用对象池实现

1. 创建对象池工厂
class ObjectPoolFactory {
  constructor(createObjFn) {
    this.objectPool = []
    this.createObjFn = createObjFn
  }
  create() {
    let obj = this.objectPool.length 
      ? this.objectPool.shift()
      : this.createObjFn.apply(this, arguments)
      return obj
  }
  recover(obj) {
    this.objectPool.push(obj)
  }
}
2. 应用
  • 创建装载一些iframe的对象池
function createIframe() {
  return () => {
    let iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    iframe.onload = () => {
      iframe.onload = null // 防止重复加载
      iframeFactory.recover(iframe) // 加载完回收节点
    }
    return iframe
  }
}
let iframeFactory = new ObjectPoolFactory(createIframe())
var iframe1 = iframeFactory.create()
iframe1.src = 'http://baidu.com'
var iframe2 = iframeFactory.create()
iframe2.src = 'http://QQ.com'
setTimeout(() => {
  let iframe3 = iframeFactory.create()
  iframe3.src = 'http://163.com'
}, 3000)
posted on 2023-05-09 10:51  pleaseAnswer  阅读(7)  评论(0编辑  收藏  举报