JavaScript设计模式与开发实践(八)享元模式

享元模式

享元模式要求将对象的属性分为内部状态和外部状态,其目标是尽量减少共享对象的数量。

示例

比如我们要实现一个批量上传的功能,一个基本的版本是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Upload {
constructor(uploadType, fileName, fileSize) {
this.uploadType = uploadType
this.fileName = fileName
this.fileSize = fileSize
this.dom = null
this.id = null
}
init(id) {
this.id = id
this.dom = document.createElement('div')
this.dom.innerHTML = `<span>文件名称:${this.fileName}</span><span>文件大小:${this.fileSize}</span><button class="delFile">删除</button>`
this.dom.querySelector('.delFile').addEventListener('click', () => {
this.delFile()
})
document.body.appendChild(this.dom)
}
delFile() {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}
let id = 0;
window.startUpload = function (uploadType, files) {
files.forEach(file => {
const uploadObj = new Upload(uploadType, file.fileName, file.fileSize)
uploadObj.init(id++)
})
};

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

每上传一个文件就会新创建一个对象(我们可以通过开发者工具的内存来查看生成对象的数量。),如果上传的文件数目很多,那么就会造成卡顿甚至卡死。

我们来使用享元模式重构它,首先我们要明确这里的内部状态和外部状态都有哪些。

  • 内部状态 uploadType 上传文件前就需要确定上传文件的方式 是插件还是flash还是其他。
  • 外部状态 fileName fileSize 这两个都会根据文件的不同而变化,因此这两个属性是外部状态。

之后需要将外部状态和内部状态分离,Upload类中只保留内部状态,外部状态交给外部管理器进行管理。而因为不需要一开始就构造Upload实例,因此我们可以采用工厂的方式实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class Upload {
constructor(uploadType) {
this.uploadType = uploadType
}
delFile(id) {
UploadManage.setExternalState(id, this)
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}

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

class UploadManage {
static uploadDataBase = {};
static 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)
this.uploadDataBase[id] = {
fileName, fileSize, dom
}
return flyWeightObj
}
static setExternalState(id, uploadObj) {
const file = this.uploadDataBase[id]
for (const key in file) {
uploadObj[key] = file[key]
}
}
}
let id = 0;
window.startUpload = function (uploadType, files) {
files.forEach(file => {
const { fileName, fileSize } = file
UploadManage.add(id++, uploadType, fileName, fileSize)
})
};

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

可以看到现在内存中只存在两个Upload的实例,分别对应plugin和flash。

实用性

显而易见,虽然创建的Upload实例从6个减少为2个,且永远是2个,但是我们需要额外维护一个Upload的实例化工厂和一个外部状态管理器。

因此虽然他可以对性能进行很好地优化,但是需要因地制宜,在某些情况下不需要引入享元模式,否则反而增加了程序的复杂性。

在满足这些条件的情况下适合引入享元模式:

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

而如果没有外部状态或者没有内部状态:

  • 没有外部状态:相当于Java或者C++中的对象池
    此处分别 new 了两个字符串对象 a1 和 a2,String a1 = new String ( "a" ). intern (); 表示如果值为 a 的字符串对象已经存在于对象池中,则返回这个对象的引用,反之则将字符串对象加入对象池并返回引用。

    1
    2
    3
    4
    5
    6
    7
    public class Test { 
    public static void main( String args[] ){
    String a1 = new String( "a" ).intern();
    String a2 = new String( "a" ).intern();
    System.out.println( a1 == a2 ); // true
    }
    }
  • 没有内部状态:相当于单例模式(没有内部状态相当于就一个状态)

对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后再进入池子等待被下次获取。

对象池技术的应用非常广泛,HTTP 连接池和数据库连接池以及如何避免频繁创建和删除DOM。

如下实现了一个简单的dom元素复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class objectPoolFactory {
objectPool = []
constructor(createObjFn) {
this.createObjFn = createObjFn
}
create() {
const obj = this.objectPool.length === 0 ?
this.createObjFn.apply(this, arguments) : this.objectPool.shift();
return obj;
}
recover(obj) {
this.objectPool.push(obj);
}
}
const iframeFactory = new objectPoolFactory(function () {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = function () {
iframe.onload = null; // 防止 iframe 重复加载的 bug
iframeFactory.recover(iframe); // iframe 加载完成之后回收节点
}
return iframe;
});

const iframe1 = iframeFactory.create();
iframe1.src = 'http://localhost:4000/jing-jiu/';
const iframe2 = iframeFactory.create();
iframe2.src = 'http://localhost:4000/jing-jiu/archives';
setTimeout(function () {
const iframe3 = iframeFactory.create();
iframe3.src = 'http://localhost:4000/jing-jiu/tags';
console.log('复用');
}, 3000);

JavaScript设计模式与开发实践(八)享元模式
https://jing-jiu.github.io/jing-jiu/2023/01/13/notebooks/JavaScript设计模式与实践/设计模式(八)/
作者
Jing-Jiu
发布于
2023年1月13日
许可协议