享元模式的定义
享元模式(Flyweight):运用共享的技术有效地支持大量细粒度的对象。
用途:性能优化
- 第一种是应用在数据层上,主要是应用在内存里大量相似的对象上;
- 第二种是应用在DOM层上,享元可以用在中央事件管理器上用来避免给父容器里的每个子元素都附加事件句柄
使用场景:
(1)程序中使用大量的相似对象,造成很大的内存开销
(2)对象的大多数状态都可以变为外部状态,剥离外部状态之后,可以用相对较少的共享对象取代大量对象
优点:
- 减少内存开销
- 提供了一种方便的管理大量相似对象的方法
缺点:
- 享元模式需要分离内部状态和外部状态,会使逻辑变得更加复杂
- 外部状态被分离出去后,访问会产生轻微的额外时耗(时间换空间)
- 对象数量少的情况,可能会增大系统的开销,实现的复杂度较大!
享元模式的基本概念
内部状态:在享元对象内部并且不会随着环境改变而改变的共享部分(内部状态是对象本身的属性,比如书的名字、作者、ISBN等等)
外部状态:随着环境改变而改变,不可共享的状态(外部状态是管理这些对象所需的额外的属性,如2本相同的书被2个人借走了,即同一对象,有不同的借书人、借阅时间等)
享元工厂:负责创建并管理享元,实现共享逻辑(创建时判断是否存在,已存在就返回现有对象,否则创建一个)
客户端:负责调用享元工厂,并存储管理相似对象所需的额外属性(比如书的id,借/还日期,是否在馆等等)
演示范例1 —— 享元模式优化图书管理
// 书的属性 // id // title // author // genre // page count // publisher id // isbn // 管理所需的额外属性 // checkout date // checkout member // due return date // availability // 享元(存储内部状态) function Book(title, author, genre, pageCount, publisherId, isbn) { this.title = title; this.author = author; this.genre = genre; this.pageCount = pageCount; this.publisherId = publisherId; this.isbn = isbn; } // 享元工厂(创建/管理享元) var BookFactory = (function() { var existingBooks = {}; var existingBook = null; return { createBook: function(title, author, genre, pageCount, publisherId, isbn) { // 如果书籍已经创建,,则找到并返回 // !!强制返回bool类型 existingBook = existingBooks[isbn]; if (!!existingBook) { return existingBook; } else { // 如果不存在选择创建该书的新实例并保存 var book = new Book(title, author, genre, pageCount, publisherId, isbn); existingBooks[isbn] = book; return book; } } } })(); // 客户端(存储外部状态) var BookRecordManager = (function() { var bookRecordDatabase = {}; return { // 添加新书到数据库 addBookRecord: function(id, title, author, genre, pageCount, publisherId, isbn, checkoutDate, checkoutMember, dueReturnDate, availability) { var book = BookFactory.createBook(title, author, genre, pageCount, publisherId, isbn); bookRecordDatabase[id] = { checkoutMember: checkoutMember, checkoutDate: checkoutDate, dueReturnDate: dueReturnDate, availability: availability, book: book } }, updateCheckStatus: function(bookId, newStatus, checkoutDate, checkoutMember, newReturnDate) { var record = bookRecordDatabase[bookId]; record.availability = newStatus; record.checkoutDate = checkoutDate; record.checkoutMember = checkoutMember; record.dueReturnDate = newReturnDate; }, extendCheckoutPeriod: function(bookId, newReturnDate) { bookRecordDatabase[bookId].dueReturnDate = newReturnDate; }, isPastDue: function(bookId) { var currDate = new Date(); return currDate.getTime() > Date.parse(bookRecordDatabase[bookId].dueReturnDate); } }; })(); // isbn号是书籍的唯一标识,以下三条只会创建一个book对象 BookRecordManager.addBookRecord(1, 'x', 'x', 'xx', 300, 10001, '100-232-32'); // new book BookRecordManager.addBookRecord(1, 'xx', 'xx', 'xx', 300, 10001, '100-232-32'); BookRecordManager.addBookRecord(1, 'xxx', 'xxx', 'xxx', 300, 10001, '100-232-32');
如果需要管理的书籍数量非常大,那么使用享元模式节省的内存将是一个可观的数目
演示范例2 —— 享元模式+对象池技术优化页面渲染
比如页面上最多显示20个DOM,则创建20个DOM用来给真正的实例去共享
通过监听滚动事件,实现在滚动的时候加载相应的数据,同时DOM被复用,B站的弹幕列表就是用了相似的技术实现的
const books = new Array(10000).fill(0).map((v, index) => { return Math.random() > 0.5 ? { name: `计算机科学${index}`, category: '技术类' } : { name: `傲慢与偏见${index}`, category: '文学类类' } }) class FlyweightBook { constructor(category) { this.category = category } // 用于享元对象获取外部状态 getExternalState(state) { for(const p in state) { this[p] = state[p] } } print() { console.log(this.name, this.category) } } // 然后定义一个工厂,来为我们生产享元对象 // 注意,这段代码实际上用了单例模式,每个享元对象都为单例, 因为我们没必要创建多个相同的享元对象 const flyweightBookFactory = (function() { const flyweightBookStore = {} return function (category) { if (flyweightBookStore[category]) { return flyweightBookStore[category] } const flyweightBook = new FlyweightBook(category) flyweightBookStore[category] = flyweightBook return flyweightBook } })() // DOM的享元对象 class Div { constructor() { this.dom = document.createElement("div") } getExternalState(extState, onClick) { // 获取外部状态 this.dom.innerText = extState.innerText // 设置DOM位置 this.dom.style.top = `${extState.seq * 22}px` this.dom.style.position = `absolute` this.dom.onclick = onClick } mount(container) { container.appendChild(this.dom) } } const divFactory = (function() { const divPool = []; // 对象池 return function(innerContainer) { let div if (divPool.length <= 20) { div = new Div() divPool.push(div) } else { // 滚动行为,在超过20个时,复用池中的第一个实例,返回给调用者 div = divPool.shift() divPool.push(div) } div.mount(innerContainer) return div } })() // 外层container,用户可视区域 const container = document.createElement("div") // 内层container, 包含了所有DOM的总高度 const innerContainer = document.createElement("div") container.style.maxHeight = '400px' container.style.width = '200px' container.style.border = '1px solid' container.style.overflow = 'auto' innerContainer.style.height = `${22 * books.length}px` // 由每个DOM的总高度算出内层container的高度 innerContainer.style.position = `relative` container.appendChild(innerContainer) document.body.appendChild(container) function load(start, end) { // 装载需要显示的数据 books.slice(start, end).forEach((bookData, index) => { // 先生产出享元对象 const flyweightBook = flyweightBookFactory(bookData.category) const div = divFactory(innerContainer) // DOM的高度需要由它的序号计算出来 div.getExternalState({innerText: bookData.name, seq: start + index}, () => { flyweightBook.getExternalState({name: bookData.name}) flyweightBook.print() }) }) } load(0, 20) let cur = 0 // 记录当前加载的首个数据 container.addEventListener('scroll', (e) => { const start = container.scrollTop / 22 | 0 if (start !== cur) { load(start, start + 20) cur = start } })
以上代码仅仅使用了2个享元对象,21个DOM对象,就完成了10000条数据的渲染,相比起建立10000个book对象和10000个DOM,性能优化是非常明显的。
演示范例3 —— 享元模式优化实现文件上传
var Upload = function(uploadType) { this.uploadType = uploadType; } /* 删除文件(内部状态) */ Upload.prototype.delFile = function(id) { uploadManger.setExternalState(id, this); // 把当前id对应的外部状态都组装到共享对象中 // 大于3000k提示 if(this.fileSize < 3000) { return this.dom.parentNode.removeChild(this.dom); } if(window.confirm("确定要删除文件吗?" + this.fileName)) { return this.dom.parentNode.removeChild(this.dom); } } /** 工厂对象实例化 * 如果某种内部状态的共享对象已经被创建过,那么直接返回这个对象 * 否则,创建一个新的对象 */ var UploadFactory = (function() { var createdFlyWeightObjs = {}; return { create: function(uploadType) { if(createdFlyWeightObjs[uploadType]) { return createdFlyWeightObjs[uploadType]; } return createdFlyWeightObjs[uploadType] = new Upload(uploadType); } }; })(); /* 管理器封装外部状态 */ var uploadManger = (function() { var uploadDatabase = {}; return { add: function(id, uploadType, fileName, fileSize) { var flyWeightObj = UploadFactory.create(uploadType); var 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; }, setExternalState: function(id, flyWeightObj) { var uploadData = uploadDatabase[id]; for(var i in uploadData) { // 直接改变形参(新思路!!) flyWeightObj[i] = uploadData[i]; } } }; })(); /*触发上传动作*/ var id = 0; window.startUpload = function(uploadType, files) { for(var i=0,file; file = files[i++];) { var uploadObj = uploadManger.add(++id, uploadType, file.fileName, file.fileSize); } }; /* 测试 */ startUpload("plugin", [ { fileName: '1.txt', fileSize: 1000 },{ fileName: '2.txt', fileSize: 3000 },{ fileName: '3.txt', fileSize: 5000 } ]); startUpload("flash", [ { fileName: '4.txt', fileSize: 1000 },{ fileName: '5.txt', fileSize: 3000 },{ fileName: '6.txt', fileSize: 5000 } ]);
更多设计模式详见——js设计模式【详解】总目录
https://blog.csdn.net/weixin_41192489/article/details/116154815