Skip to content

AHABHGK

享元模式

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引:

  • 内部状态存储于对象内部。

  • 内部状态可以被一些对象共享。

  • 内部状态独立于具体的场景,通常不会改变。

  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。

剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。

文件上传

两种上传类型:plugin、flash

1class Upload {
2 constructor(uploadType, fileName, fileSize) {
3 this.uploadType = uploadType
4 this.fileName = fileName
5 this.fileSize = fileSize
6 this.dom = null
7 }
8
9 init(id) {
10 this.id = id
11 this.dom = document.createElement('div')
12 this.dom.innerHTML = `
13 <span>文件名称:${this.fileName}, 文件大小:${this.fileSize}</span>
14 <button class="delFile">删除</button>
15 `
16 this.dom.querySelector('.delFile').onclick = () => {
17 this.delFile()
18 }
19 document.body.appendChild(this.dom)
20 }
21
22 delFile() {
23 this.dom.parentNode.removeChild(this.dom)
24 }
25}
26
27let id = 0
28
29window.startUpload = function (uploadType, files) {
30 files.foeEach(file => {
31 const uploadObj = new Upload(uploadType, file.fileName, file.fileSize)
32 uploadObj.init(id++)
33 })
34}
35
36startUpload('plugin', [
37 {
38 fileName: 'one.txt',
39 fileSize: 1000,
40 },
41 {
42 fileName: 'two.txt',
43 fileSize: 3000,
44 },
45])
46
47startUpload('flash', [
48 {
49 fileName: 'three.png',
50 fileSize: '5000',
51 },
52 {
53 fileName: 'four.png',
54 fileSize: 4000,
55 },
56])

可见每个文件都要有一个 Upload 对象,当上传的文件极多时,会开销极大甚至造成内存不足

可以用享元模式优化:

1class Upload {
2 constructor(uploadType) {
3 this.uploadType = uploadType
4 }
5
6 delFile(id) {
7 uploadManager.setExternalState(id, this)
8 this.dom.parentNode.removeChild(this.dom)
9 }
10}
11
12const UploadFactory = (function () {
13 const createdFlyWeightObjs = {}
14 return {
15 create(uploadType) {
16 if (createdFlyWeightObjs[uploadType]) {
17 return createdFlyWeightObjs[uploadType]
18 }
19 return createdFlyWeightObjs[uploadType] = new Upload(uploadType)
20 }
21 }
22})()
23
24const uploadManager = (function () {
25 const uploadDatabase = {}
26
27 return {
28 add(id, uploadType, fileName, fileSize) {
29 const flyWeightObj = UploadFactory.create(uploadType)
30 const dom = document.createElement('div')
31
32 dom.innerHTML = `
33 <span>文件名称:${fileName}, 文件大小:${fileSize}</span>
34 <button class="delFile">删除</button>
35 `
36 dom.querySelector('.delFile').onclick = () => {
37 flyWeightObj.delFile(id)
38 }
39 document.body.appendChild(this.dom)
40
41 uploadDatabase[id] = {
42 fileName,
43 fileSize,
44 dom,
45 }
46
47 return flyWeightObj
48 },
49
50 setExternalState(id, flyWeightObj) {
51 const uploadData = uploadDatabase[id]
52 for (const i in uploadData){
53 flyWeightObj[i] = uploadData[i]
54 }
55 },
56 }
57})()
58
59let id = 0
60
61window.startUpload = function (uploadType, files) {
62 files.forEach(file => {
63 uploadManager.add(id++, uploadType, file.fileName, file.fileSize)
64 })
65}
66
67// 使用同上

享元模式重构之前的代码里一共创建了 6 个 upload 对象,而通过享元模式重构之后,对象的数量减少为 2,更幸运的是,就算现在同时上传 2000 个文件,需要创建的 upload 对象数量依然是 2

很容易让人联想到 JavaScript 的原型:

1const PluginUploader = function (fileName, fileSize) {
2 this.fileName = fileName
3}
4PluginUploader.prototype.uploadType = 'plugin' // 享元
5PluginUploader.prototype.add = function () {
6 // ...
7}
8
9const FlashUploader = function (fileName, fileSize) {
10 this.fileName, fileSize
11}
12FlashUploader.prototype.uploadType = 'flash' // 享元
13FlashUploader.prototype.add = function () {
14 // ...
15}

之前虽然只有两个 upload 对象(内部状态),但对文件的描述对象(外部状态)仍然不会减少,这里的 prototype 用于做内部状态,一共也是两个,构造函数通过传参构造出外部状态

由此用原型模式实现了享元模式,可见很多设计模式之间是相通的,而连通它们的是为了提高代码可维护性的设计原则

使用场景

一般来说,以下情况发生时便可以使用享元模式:

  • 一个程序中使用了大量的相似对象

  • 由于使用了大量对象,造成很大的内存开销

  • 对象的大多数状态都可以变为外部状态

  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象

对象池

1const objectPoolFactory = function (createObjFn) {
2 const objectPool = []
3
4 return {
5 create(...args) {
6 const obj = objectPool.length === 0
7 ? createObjFn(...args)
8 : objectPool.shift()
9 return obj
10 },
11 recover(obj) {
12 objectPool.push(obj)
13 },
14 }
15}
16
17// using
18const iframeFactory = objectPoolFactory(function () {
19 const iframe = document.createElement('iframe')
20 document.body.appendChild(iframe)
21 iframe.onload = function () {
22 iframe.onload = null // 防止 iframe 重复加载的 bug
23 iframeFactory.recover(iframe) // iframe 加载完成之后回收节点
24 }
25 return iframe
26})
27
28const iframe1 = iframeFactory.create()
29iframe1.src = 'http://baidu.com'
30
31const iframe2 = iframeFactory.create()
32iframe2.src = 'http://QQ.com'
33
34setTimeout(() => {
35 const iframe3 = iframeFactory.create()
36 iframe3.src = 'http://163.com'
37}, 3000)

通过创建时的 create,onload 时的 recover,实现只用一个 iframe 对象在网页上创建多个 iframe,减小了空间上的损耗