JavaScript設計模式之享元模式

定義

享元模式是一種用於性能優化的模式,享元模式的核心是運用共享對象的技術來有效支持大量細粒度的對象。若是系統由於建立了大量對象而致使內存佔用太高,享元模式就能發揮做用了。javascript

一個簡單的例子

假設有個製衣工廠,目前的產品有50種男款衣服和50種女款衣服,爲了推銷產品,工廠決定生產一些塑料模特來穿上他們的衣服拍成廣告照片,正常狀況下須要50個男模特和50個女模特,用程序表達:html

class Model {
  construct (sex, underwear) {
    this.sex = sex
    this.underwear = underwear
  }

  takePhoto () {
    console.log(`sex ${this.sex} underwear ${this.underwear}`)
  }
}

for (let i = 0; i < 50; i++) {
  const maleModel = new Model('male', 'underwear' + i)
  maleModel.takePhoto()
}

for (let j = 0; j < 50; j++) {
  const femaleModel = new Model('male', 'underwear' + j)
  femaleModel.takePhoto()
}
複製代碼

上述代碼產生了一百個對象,若是未來有10000種的男款和10000種女款的衣服,那程序可能由於存在如此多的對象而崩潰。

換種思路,上面的例子中最須要區分的是男女模特,那咱們把其它參數從構造函數中移除,只接受sex參數:前端

class Model {
  construct (sex) {
    this.sex = sex
  }

  takePhoto () {
    console.log(`sex ${this.sex} underwear ${this.underwear}`)
  }
}
複製代碼

分別建立一個男模特和一個女模特:java

const maleModel = new Model('male')
const femaleModel = new Model('female')
複製代碼

給男模特依次穿上不一樣的衣服,並拍照:數據庫

for (let j = 0; j < 50; j++) {
  maleModel.underwear = 'underwear' + j
  femaleModel.takePhoto()
}
複製代碼

女模特穿衣拍照相似,能夠看到,改進代碼以後,只須要兩個對象便完成了一樣的功能。數組

內部狀態和外部狀態

上面的例子是享元模式的雛形,享元模式要求將對象的屬性劃分爲內部狀態和外部狀態,狀態在這裏通常指的是對象的屬性。在上面的例子中,內部狀態就是模特的性別,外部狀態對應模特會變化的不一樣款的衣服。那麼如何劃份內部狀態和外部狀態了?下面有幾條經驗能夠提供一些指導。性能優化

  • 內部狀態存儲於對象內部
  • 內部狀態能夠被對象共享
  • 內部狀態一般獨立於具體的場景,一般不會變
  • 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享

這樣的話,咱們就能夠把內部狀態相同的對象指定爲同一個共享的對象,而外部狀態從對象中剝離出來,儲存在外部。雖然結合外部狀態組裝成一個完整的對象的過程須要花費必定的時間,可是卻能夠大大減小系統中對象的數量,因此享元模式也是一種使用時間來換取空間的優化模式。app

文件上傳

對象爆炸
在一個上傳模塊的開發中,若是沒有用享元模式優化,會出現由於對象數量爆炸而到致使內存佔用過大的問題。文件上傳功能雖然能夠依照隊列,一個一個地排隊上傳,但也支持同時選擇2000個文件,每個文件都對應着一個JavaScript上傳對象的建立。下面先看下代碼實現:dom

let id = 0

window.startUpload = function (uploadType, files) {
  for (let i = 0, file; file=files[i++]) {
    const uploadObj = new Upload(uploadType, file.fileName, file.fileSize)
    uploadObj.init(id++)
  }
}
複製代碼

下面實現Upload類:函數

class Upload {
  construct (uploadType, fileName, fileSize) {
    this.uploadType = uploadType
    this.fileName = fileName
    this.fileSize = fileSize
    this.dom = null
  }

  init (id) {
    const self = this
    this.id = id
    this.dom = document.createElement('div')
    this.dom.innerHTML = `<span>文件名稱:${this.fileName},文件大小:${this.fileSize}</span><button class="delfile">刪除</button>`

    this.dom.querySelector('.delfile').onclick = function () {
      self.delFile()
    }
    document.body.appendChild(this.dom)
  }

  delFile () {
    if (this.fileSize < 300) {
      return this.dom.parentNode.removeChild(this.dom)
    }
    if (window.confirm(`肯定要刪除文件嗎?${this.fileName}`)) {
      return this.dom.parentNode.removeChild(this.dom)
    }
  }
}
複製代碼

而後咱們就能夠這樣使用:

startUpload('plugin', [
  {
    fileName: '1.txt',
    fileSize: 1000
  },
  {
    fileName: '2.txt',
    fileSize: 3000
  },
  {
    fileName: '3.html',
    fileSize: 4000
  }
])

startUpload('flash', [
  {
    fileName: '1.txt',
    fileSize: 1000
  },
  {
    fileName: '2.txt',
    fileSize: 3000
  },
  {
    fileName: '3.html',
    fileSize: 4000
  }
])
複製代碼

從上面的代碼中能夠看出,有多少須要上傳的文件,就須要建立多少個upload對象,因此咱們能夠用享元模式重構它。先確認好對象的內部狀態和外部狀態,從上面的例子中,咱們能夠看出,upload必須依賴uploadType屬性才能工做,由於插件上傳、Flash上傳、表單上傳的工做原理區別很大,它們各自調用的接口也不同,因此在建立upload對象時,必須明確到底使用哪一種上傳類型。下面看具體實現代碼。
剝離外部狀態

class Upload {
  construct (uploadType) {
    this.uploadType = uploadType
  }

  delFile (id) {
    uploadManager.setExternalState(id, this)

    if (this.fileSize < 300) {
      return this.dom.parentNode.removeChild(this.dom)
    }
    if (window.confirm(`肯定要刪除文件嗎?${this.fileName}`)) {
      return this.dom.parentNode.removeChild(this.dom)
    }
  }
}
複製代碼

工廠進行上傳對象實例化

const UploadFactory = (function () {
  const createdFlyWeightObjects = {}
  return {
    create (uploadType) {
      if (createdFlyWeightObjects[uploadType]) {
        return createdFlyWeightObjects[uploadType]
      }
      return createdFlyWeightObjects[uploadType] = new Upload(uploadType)
    }
  }
})()
複製代碼

管理封裝外部狀態

const uploadManager = (function () {
  const uploadDatabase = {}

  return {
    add (id, uploadType, fileName, fileSize) {
      const flyWeightObj = UploadFactory.create(uploadType)

    const dom = document.createElement('div')
    dom.innerHTML = `<span>文件名稱:${fileName},文件大小:${fileSize}</span><button class="delfile">刪除</button>`

    dom.querySelector('.delfile').onclick = function () {
      flyWeightObj.delFile(id)
    }

    document.body.appendChild(dom)

    uploadDatabase[id] = {
      fileName,
      fileSize,
      dom
    }

    return flyWeightObj
    },
    setExternalState (id, flyWeightObj) {
      const uploadData = uploadDatabase[id]
      for (const key in uploadData) {
        flyWeightObj[key] = uploadData[key]
      }
    }
  }
})()
複製代碼

最後改寫startUpload函數:

let id = 0

window.startUpload = function (uploadType. files) {
  for (let i = 0, file; file = files[i++]) {
    const uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
  }
}
複製代碼

最後測試使用:

startUpload('plugin', [
  {
    fileName: '1.txt',
    fileSize: 1000
  },
  {
    fileName: '2.txt',
    fileSize: 3000
  },
  {
    fileName: '3.html',
    fileSize: 4000
  }
])

startUpload('flash', [
  {
    fileName: '1.txt',
    fileSize: 1000
  },
  {
    fileName: '2.txt',
    fileSize: 3000
  },
  {
    fileName: '3.html',
    fileSize: 4000
  }
])
複製代碼

使用享元模式重構以前,一共建立了6個upload對象,而重構以後,對象的數量減小爲2,並且就算上傳的文件有2000個,upload對象數量依舊是2。

對象池

對象池維護一個裝載空閒對象的池子,若是須要對象的時候,不是直接建立,而是從對象池裏獲取。若是對象池裏沒有空閒對象,則建立一個新的對象,當獲取的對象完成它的職責以後,再進入池子等待下次獲取。對象池的應用普遍,HTTP鏈接池和數據庫鏈接池都是表明應用,在Web前端開發中,對象池使用的場景大可能是跟DOM相關,由於建立DOM和操做DOM既耗費空間也耗費時間。

地圖小氣泡對象池
假設咱們開發一個地圖應用,地圖上常常會出現一些標誌地面建築的小氣泡,咱們稱呼爲tooltip。假設咱們在搜附近的網吧時地圖上出現了兩個小氣泡,然再搜附近的便利店,頁面上出現了6個氣泡。使用對象池實現的思想,第一次搜建立的2個氣泡不會被刪除,而是它們放在對象池中,在第二次搜索的時候,就能夠複用前面2個,只須要再建立4個氣泡。下面看代碼實現:

const tooltipFactory = (function () {
  const tooltipPool = []

  return {
    create () {
      // 對象池爲空則建立
      if (tooltipPool.length === 0) {
        const div = document.createElement('div')
        document.body.appendChild(div)
        return div
      } else {
        return tooltipPool.shift()  // 從對象池裏取出一個
      }
    },
    recover (tooltip) {
      return tooltipPool.push(tooltip)
    }  
  }
})()
複製代碼

第一次搜索的時候,建立兩個tooltip,建立ary數組保存tooltip,方便下次搜索繪製前回收:

let ary = []
const tooltips = ['A', 'B']
for (let i = 0, len = tooltips.length; i < len; i++) {
  const tooltip = tooltipFactory.create()
  tooltip.innerHTML = tooltips[i]
  ary.push(tooltip) 
}
複製代碼

第二次搜索繪製前,先回收前面兩個tooltip:

for (let i = 0, len = ary.length; i < len; i++) {
  tooltipFactory.recover(ary[i])
}
複製代碼

再建立6個氣泡:

const tooltips = ['A', 'B', 'C', 'D', 'E', 'F']
for (let i = 0, len = tooltips.length; i < len; i++) {
  const tooltip = tooltipFactory.create()
  tooltip.innerHTML = tooltips[i]
}
複製代碼

對象池跟享元模式思想有點類似,雖然innerHTML的值也能夠看做tooltip的外部狀態,但在這裏咱們並無主動分離內部狀態和外部狀態。

通用對象池的實現

const objectPollFactory = function (createObjFn) {
  const objectPool = []

  return {
    create () {
      // 對象池爲空則建立
      const obj = objectPool.length === 0 ? 
      createObjFn.apply(this, arguments) : objectPool.shift()
      
      return obj
    },
    recover (obj) {
      return objectPool.push(obj)
    }  
  }
}
複製代碼

對象池是另外一種性能優化方案,它跟享元模式有點相似,但沒有分離內部狀態和外部狀態這個過程。

總結

享元模式是一種很好的性能優化方案,但也會帶來一些複雜性的問題,從文件上傳的例子咱們能夠看出,使用了享元模式,咱們須要多維護一個factory對象和一個manager對象,在不使用享元模式的環境下,這些開銷是能夠避免的。享元模式帶來的好處很大程度取決於如何使用以及什麼時候使用,當你的項目出現如下狀況比較適合使用享元模式:

  • 一個程序中使用了大量的類似對象
  • 使用了大量對象後,形成很大的內存開銷
  • 對象的大多數狀態能夠變爲外部狀態
  • 剝離出對象的外部狀態後,能夠用相對較少的共享對象取代大量對象

使用享元模式的關鍵是把內部狀態和外部狀態分離開來,有多少種內部狀態的組合,系統中並最多存在多少個共享對象。

相關文章
相關標籤/搜索