[JS设计模式]享元模式

205 阅读5分钟

前言

设计模式的创造主要实现的是,解耦代码提高可维护性和优化代码性能。之前写的文章中大部分的设计模式都是侧重解耦代码,而今天讲的享元模式则是可以优化代码性能的一种设计模式。

设计思路

假如现在我们是一个专门从事在机场接待的工作人员。我们手上有一块板子,写的是我们要接的人的名字。假如我们今天的任务是要接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设计模式与开发实践》—— 曾探