前言
设计模式的创造主要实现的是,解耦代码提高可维护性和优化代码性能。之前写的文章中大部分的设计模式都是侧重解耦代码,而今天讲的享元模式则是可以优化代码性能的一种设计模式。
设计思路
假如现在我们是一个专门从事在机场接待的工作人员。我们手上有一块板子,写的是我们要接的人的名字。假如我们今天的任务是要接2个人,他们会先后到达。这个流程的代码应该如下:
class Board{
constructor(name){
this.name = name;
}
}
const customers = ['tom','john'];
for(const name of customers){
const board = new Board(name);
}
以传统的面向对象习惯,很自然会写出上述的代码。可是这时候会出现一个问题,假如每天要接待的顾客有上百上千个。这时候我们要创建的对象就会增加,我们都知道在new 一个对象的开销成本是很大的,特别是当我们的JavaScript代码运行在移动端或者弱pc上时。
享元模式
享元模式就是专门处理这种情况的,一般如果满足下列的情况都可以考虑用享元模式。
- 一个程序中使用了大量的相似对象
- 由于使用了大量对象,造成很大的内存开销
- 对象的大多数状态都可以变为外部状态
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象
简单来说就是,上述的解决方式是有多少个顾客,我们就要准备多少块板子。而享元模式的解决方式是,只准备一块板子,当需要写不同顾客名字的时候直接把名字改掉。
class Board{
constructor(name){
this.name = name;
}
// 增加一个修改板子名字的方法
changeName(name){
this.name = name;
}
}
const customers = ['tom','john'];
// 只创建一个实例
const board = new Board();
for(const name of customers){
// 需要时修改板子名字
board.changeName(name);
}
通过这种方式,就避免了大量重复的new,从而提高代码性能。
内部状态与外部状态
要深入掌握享元模式,就要懂得区分【内部状态】与【外部状态】。这其实也很好理解,由于我们需要实现的场景是一个对象的重复利用,因此我们要懂得把这个对象的状态区分出哪些是可以共用的,哪些是需要替换的。
还是以刚刚的例子,我们可以思考一下这个接待板有什么状态是内部的,有什么是外部的。接待板显示的名字当然是外部的,因为他需要根据接待不同的顾客修改(使用场景)。而接待板的背光功能应该是内部的,也就是说不管接待谁,这个状态都是可以保持的。
class Board{
constructor(name){
// 添加背光功能
this.light = false;
this.name = name;
}
triggerLight(){
this.light = !this.light;
}
// 增加一个修改板子名字的方法
changeName(name){
this.name = name;
}
}
const customers = ['tom','john'];
// 只创建一个实例
const board = new Board();
// 打开背光灯(内部状态)
board.triggerLight();
for(const name of customers){
// 需要时修改板子名字(外部状态)
board.changeName(name);
}
使用例子
举一个例子,这也是我在开发中真实遇到的一个情况。在element-ui中有一个组件叫Tooltip。它是ui效果如下:
每当用鼠标指向目标时,就会出现一个小浮标。同时我们可以在右侧的开发者工具中看到,element-ui实际上为每一个浮标都创建了一个实例。而我当时遇到的需要是给一个列表的每一项都加上这个浮标,而这个列表的项可能会很大。因此很快就出现了性能问题(有上百个浮标实例被创建了)。
而这种情况就很适合用享元模式优化。
class Tooltip(){
constructor(){
this.show = false;
this.top = 0;
this.left = 0;
}
// 在需要显示时,直接修改定位
show(top,left){
this.show = true;
this.top = top;
this.left = left;
}
// 隐藏
hide(){
this.show = false;
}
}
当时我确实是用这个方案解决的,而在element-ui中由于组件已经被封装了,所有需要手动通过ref拿到实例的定位来修改。
对象池
享元模式带来的性能优化是十分明显的,但普通的享元模式是有局限的。还是以接待板举例,如果某一次接待的客人是两个或者两个以上同时出现的,那这个时候就肯定不可以用一块接待板就能解决的。但是对于这种情况我们还是有对应的优化方案的,那就是——对象池。
对象池的本质是一个对象管理,在对象不足时会新建实例,当下次需要时可以直接从池中取出。
var poor = [];
class Tooltip(){
constructor(){
this.show = false;
this.top = 0;
this.left = 0;
}
// 在需要显示时,直接修改定位
show(top,left){
this.show = true;
this.top = top;
this.left = left;
}
// 隐藏
hide(){
this.show = false;
}
}
const target = [{top:10,left:10},{top:20,left:20}];
for(var i=0; i< target.length; i++){
const position = target[i];
let tooltip;
// 如果池中有就直接取
if(poor[i]){
tooltip = poor[i];
}else{
// 如果池中没有就新建一个实例
tooltip = new Tooltip();
poor.push(tooltip);
}
tooltip.show(position.top,position.left);
}
上述代码是描述对象池基本使用的一个简单例子,在实际开发中会比这复杂,往往还需要考虑对象池的最大容量和回收机制等。
总结
享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。大家可以思考一下,当下正在开发的业务中是否存在可以用享元模式优化的代码,希望对大家有所帮助。
参考
《JavaScript设计模式与开发实践》—— 曾探