js设计模式【详解】—— 享元模式

简介: js设计模式【详解】—— 享元模式

享元模式的定义

享元模式(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


目录
相关文章
|
1月前
|
设计模式 存储 Java
【十】设计模式~~~结构型模式~~~享元模式(Java)
文章详细介绍了享元模式(Flyweight Pattern),这是一种对象结构型模式,通过共享技术实现大量细粒度对象的重用,区分内部状态和外部状态来减少内存中对象的数量,提高系统性能。通过围棋棋子的设计案例,展示了享元模式的动机、定义、结构、优点、缺点以及适用场景,并探讨了单纯享元模式和复合享元模式以及与其他模式的联用。
【十】设计模式~~~结构型模式~~~享元模式(Java)
|
22天前
|
设计模式 JavaScript 前端开发
从工厂到单例再到策略:Vue.js高效应用JavaScript设计模式
【8月更文挑战第30天】在现代Web开发中,结合使用JavaScript设计模式与框架如Vue.js能显著提升代码质量和项目的可维护性。本文探讨了常见JavaScript设计模式及其在Vue.js中的应用。通过具体示例介绍了工厂模式、单例模式和策略模式的应用场景及其实现方法。例如,工厂模式通过`NavFactory`根据用户角色动态创建不同的导航栏组件;单例模式则通过全局事件总线`eventBus`实现跨组件通信;策略模式用于处理不同的表单验证规则。这些设计模式的应用不仅提高了代码的复用性和灵活性,还增强了Vue应用的整体质量。
14 0
|
1月前
|
设计模式 JavaScript 前端开发
小白请看 JS大项目宝典:设计模式 教你如何追到心仪的女神
小白请看 JS大项目宝典:设计模式 教你如何追到心仪的女神
|
2月前
|
设计模式 JavaScript Go
js设计模式【详解】—— 状态模式
js设计模式【详解】—— 状态模式
40 7
|
2月前
|
设计模式 JavaScript
js设计模式【详解】—— 桥接模式
js设计模式【详解】—— 桥接模式
48 6
|
2月前
|
设计模式 JavaScript
js设计模式【详解】—— 原型模式
js设计模式【详解】—— 原型模式
37 6
|
2月前
|
设计模式 JavaScript 前端开发
JavaScript进阶 - JavaScript设计模式
【7月更文挑战第7天】在软件工程中,设计模式是解决常见问题的标准解决方案。JavaScript中的工厂模式用于对象创建,但过度使用可能导致抽象过度和缺乏灵活性。单例模式确保唯一实例,但应注意避免全局状态和过度使用。观察者模式实现了一对多依赖,需警惕性能影响和循环依赖。通过理解模式的优缺点,能提升代码质量。例如,工厂模式通过`createShape`函数动态创建对象;单例模式用闭包保证唯一实例;观察者模式让主题对象通知多个观察者。设计模式的恰当运用能增强代码可维护性。
68 0
|
2月前
|
设计模式 缓存 JavaScript
js设计模式实例
【7月更文挑战第2天】JavaScript设计模式包含工厂、单例、建造者、抽象工厂和代理模式等,它们是最佳实践和可重用模板,解决创建、职责分配和通信等问题。例如,工厂模式封装对象创建,单例确保全局唯一实例,建造者模式用于复杂对象构建,抽象工厂创建相关对象集合,而代理模式则控制对象访问。这些模式提升代码质量、可读性和灵活性,是高效开发的关键。
28 0
|
4月前
|
设计模式 JavaScript 算法
js设计模式-策略模式与代理模式的应用
策略模式和代理模式是JavaScript常用设计模式。策略模式通过封装一系列算法,使它们可互换,让算法独立于客户端,提供灵活的选择。例如,定义不同计算策略并用Context类执行。代理模式则为对象提供代理以控制访问,常用于延迟加载或权限控制。如创建RealSubject和Proxy类,Proxy在调用RealSubject方法前可执行额外操作。这两种模式在复杂业务逻辑中发挥重要作用,根据需求选择合适模式解决问题。
|
设计模式 算法 JavaScript