JavaScript设计模式-享元模式
概念
-
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量
级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 -
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在
JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非
常有意义的事情。
例子
// 有一家模特儿公司(工厂)
const modelFactory = function (type, name) {
this.type = type
this.name = name
}
// 给穿上指定衣服的模特儿拍照,
modelFactory.prototype.takePhoto = function () {
console.log('给type= ' + this.type + ' name=' + this.name + '拍照')
}
// 有50件不同的西装,需要拍50张照片
for (let i = 1; i <= 50; i++) {
// 给每件西装都请一位模特儿,一共请了50个
const suitModel = new modelFactory('西装', i)
suitModel.takePhoto()
}
// 有50件不同的羽绒服,需要50张照片
for (let j = 1; j <= 50; j++) {
// 给每件羽绒服都请一位模特儿,一共请了50个
const jacketModel = new modelFactory('羽绒服', j)
jacketModel.takePhoto()
}
// 上面new了100个对象,这样是很占内存(资源),我们可以用享元模式优化
//我们只需要两位模特就行了,一个西装模特,一个羽绒服模特
const modelFactory = function (type) {
this.type = type
}
modelFactory.prototype.takePhoto = function () {
console.log('给type= ' + this.type + ' name=' + this.name + '拍照')
}
// 分别创建一个西装模特对象和一个羽绒服模特对象:
const suitModel = new modelFactory('西装')
const jacketModel = new modelFactory('羽绒服')
// 给模特依次穿上西装,并进行拍照:
for (let i = 1; i <= 50; i++) {
suitModel.name = i
suitModel.takePhoto()
}
// 给模特依次穿上羽绒服,并进行拍照:
for (let j = 1; j <= 50; j++) {
jacketModel.name = j
jacketModel.takePhoto()
}
// 这个西装模特对象和一个羽绒服模特对象就是我们的享元了
// 以上就是是享元模式的雏形
内部状态与外部状态
- 享元模式要求将对象的属性划分为内部状态与外部 状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内 部状态和外部状态,下面的几条经验提供了一些指引。
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
-
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
-
剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。
-
在上面的例子中,type是内部状态,name是外部状态,通过区分这两种状态,大大减少了系 统中的对象数量。
-
总结:享元模式共同的属性是内部属性,在创建对象的时候决定;不同的属性是外部属性,在对象创建完后改变。
享元模式的通用结构
- 示例初步展示了享元模式的威力,但这还不是一个完整的享元模式,在这个例子中 还存在以下两个问题。
-
我们通过构造函数显式new出了两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象。(不用就不创建)
-
给 model 对象手动设置了name外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难。
- 我们通过一个对象工厂来解决第一个问题,只有当某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。
例子
// 例子
// 工厂进行对象实例化
const UploadFactory = (function () {
const createdFlyWeightObjs = {}
return {
create: function (uploadType) {
if (createdFlyWeightObjs [uploadType]) {
return createdFlyWeightObjs [uploadType]
}
return createdFlyWeightObjs [uploadType] = new Upload(uploadType)
}
}
})()
// 管理器封装外部状态
const uploadManager = (function () {
// 定义上传文件的集合
const uploadDatabase = {}
return {
// 上传文件处理逻辑
add: function (id, uploadType, fileName, fileSize) {
// 利用UploadFactory.create工厂进行对象实例化,得到实例,同时,uploadType相同的,只会有一个实例
// uploadType就是内部状态了,其他属性就是外部状态了
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: fileName,
fileSize: fileSize,
dom: dom
}
return flyWeightObj
},
// 用来获取当前id的文件对象
setExternalState: function (id, flyWeightObj) {
const uploadData = uploadDatabase[id]
for (const i in uploadData) {
flyWeightObj[i] = uploadData[i]
}
}
}
})()
let id = 0
// 开始上传
const startUpload = function (uploadType, files) { // uploadType 区分是控件还是 flash
for (let i = 0; i < files.length; i++) {
const file = files[i]
uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
}
}
// 上传的对象
const Upload = function (uploadType) {
this.uploadType = uploadType
}
Upload.prototype.delFile = function (id) {
// 这里把id和this传进setExternalState方法,该方法会根据id找到上传的文件对象,并把对象属性赋值给this
// 那么下面的this.dom就可以找到相应的文件dom了
uploadManager.setExternalState(id, this) // (1)
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom)
}
}
// 接下来分别创建 3 个插件上传对象和 3 个 Flash 上传对象:
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
}
])
总结
- 享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。
- 优缺点:享元模式是一种用时间换空间的优化模式。