享元模式(Flyweight Pattern)是一种结构型设计模式,主要用于优化对象的复用,以减少创建大量类似对象所带来的开销。这种模式的核心思想是将一个对象的内部状态和外部状态区分开,内部状态是对象共享的部分,外部状态是对象独立的部分。
享元模式通常被应用于以下的应用场景:
- 系统中存在大量相似或完全相同的对象。
- 对象的创建和存储成本高。
- 对象可以轻易地被共享,而不影响其外部行为。
了解享元模式
假设有一家衣服工厂,目前的产品有 50 种男式上衣和 50 种女士上衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50 个男模特和 50 个女模特,然后让他们每人分别穿上一件上衣来拍照。不使用享元模式的情况下,在程序里也许会这样写
js
const Model = function (sex, underwear) {
this.sex = sex;
this.underwear = underwear;
};
Model.prototype.takePhoto = function () {
console.log("sex= " + this.sex + " underwear=" + this.underwear);
};
for (let i = 1; i <= 50; i++) {
const maleModel = new Model("male", "underwear" + i);
maleModel.takePhoto();
}
for (var j = 1; j <= 50; j++) {
const femaleModel = new Model("female", "underwear" + j);
femaleModel.takePhoto();
}
要得到一张照片,每次都需要传入 sex 和 underwear 参数,如上所述,现在一共有 50 种男上衣和 50 种女上衣,所以一共会产生 100 个对象。如果将来生产了 10000 种上衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。
为了减少对象的数量,我们可以使用享元模式。在这个场景中,性别是对象的内部状态,因为每种性别只需要一个模特。而上衣是外部状 态,因为它随时间和情境而变化。我们可以为每个性别只创建一个模特对象,并为这些对象动态设置上衣来拍照。
现在来改写一下代码,既然只需要区别男女模特,那我们先把 underwear 参数从构造函数中移除,构造函数只接收 sex 参数:
js
class FlyweightModel {
constructor(sex) {
this.sex = sex;
}
takePhoto(underwear) {
console.log("sex= " + this.sex + " underwear=" + underwear);
}
}
// 享元工厂
const modelFactory = (function () {
const modelPool = {};
return {
createModel: function (sex) {
if (!modelPool[sex]) {
modelPool[sex] = new FlyweightModel(sex);
}
return modelPool[sex];
},
};
})();
// 使用享元工厂创建模特对象
const maleModel = modelFactory.createModel("male");
const femaleModel = modelFactory.createModel("female");
// 使用模特对象拍照
for (let i = 1; i <= 50; i++) {
maleModel.takePhoto("underwear" + i);
}
for (let j = 1; j <= 50; j++) {
femaleModel.takePhoto("underwear" + j);
}
通过这种方式,无论有多少种上衣,我们只需要为每个性别创建一个模特对象,从而大大减少了对象的数量。
文本编辑器中的字符样式
考虑一个文本编辑器,文本中可以有成千上万的字符,每个字符可能有其特定的样式,如加粗、斜体、下划线、字体大小和颜色等。如果为每个字符都存储这些样式信息,将会浪费大量的内存。
如果不使用享元模式,每个字符都有自己的样式信息,如下代码所示:
js
class Character {
constructor(char, bold, italic, underline, color, fontSize) {
this.char = char;
this.bold = bold;
this.italic = italic;
this.underline = underline;
this.color = color;
this.fontSize = fontSize;
}
display() {
console.log(this.char);
}
}
const charA = new Character("A", true, false, true, "red", 16);
const charB = new Character("B", true, false, true, "red", 16);
charA.display();
charB.display();
在上面的代码中,尽管 charA 和 charB 具有相同的样式,但每个字符都存储了这些样式信息。
接下来我们看看使用享元模式的情况是怎么样的:
js
// 内部状态:样式
class TextStyle {
constructor(bold, italic, underline, color, fontSize) {
this.bold = bold;
this.italic = italic;
this.underline = underline;
this.color = color;
this.fontSize = fontSize;
}
}
// 外部状态:字符
class Character {
constructor(char, style) {
this.char = char;
this.style = style;
}
display() {
console.log(this.char);
}
}
// 享元工厂
class StyleFactory {
constructor() {
this.styleMap = {};
}
getStyle(bold, italic, underline, color, fontSize) {
const key = `${bold}-${italic}-${underline}-${color}-${fontSize}`;
if (!this.styleMap[key]) {
this.styleMap[key] = new TextStyle(
bold,
italic,
underline,
color,
fontSize
);
}
return this.styleMap[key];
}
}
const styleFactory = new StyleFactory();
const sharedStyle = styleFactory.getStyle(true, false, true, "red", 16);
const charA = new Character("A", sharedStyle);
const charB = new Character("B", sharedStyle);
在上面的这段代码中,定义了一个文本编辑器的简化模型,其中 TextStyle 类描述了文本的样式,Character 类表示带有特定样式的字符。为了避免创建重复的样式对象,我们使用 StyleFactory 工厂来确保相同的样式被共享和复用,从而优化内存使用。
如果遇到样式不同的情况下享元模式仍然可以发挥作用。关键在于即使样式有所不同,很多样式仍然可能是相同的或者重复的。所以,对于这些重复的样式,享元模式能够确保我们只为它们创建一个对象。
js
const styleFactory = new StyleFactory();
const redBoldStyle = styleFactory.getStyle(true, false, false, "red", 16);
const blueItalicStyle = styleFactory.getStyle(false, true, false, "blue", 16);
const charA = new Character("A", redBoldStyle);
const charB = new Character("B", redBoldStyle);
const charC = new Character("C", blueItalicStyle);
即使存在多种不同的样式,只要有一部分样式是重复的,享元模式都可以帮助我们节省内存。在实际的应用中,可能有数千甚至数百万个字符,但样式的种类可能远远少于字符数量。因此,通过共享样式对象,我们可以显著减少内存使用。
享元模式的通用结构
享元模式的核心思想是共享对象,从而减少系统中的对象数量并减少内存使用。这种模式在面对大量相似对象时特别有用。以下是享元模式的通用结构:
- 享元: 这是所有具体享元类的父类或接口,它定义了一个接口来接收和获取外部状态。在我们前面的示例中,TextStyle 就是一个具体的享元。
- 具体享元:实现享元接口并存储内部状态。这个状态必须是共享的和不可变的。多个外部状态可以共享相同的内部状态。
- 享元工厂:负责创建和管理享元对象。它确保相同的内部状态对应的对象是共享的。如果请求的对象已经存在,它就返回这个对象;否则,它创建一个新的对象。在前面的示例中,StyleFactory 就是享元工厂。
- 客户端:客户端维护对所有享元的引用,并计算或存储享元的外部状态。客户端代码通常会创建一组享元并在它们之间共享状态
- 外部状态:这些状态是依赖于具体的应用场景并由客户端存储或计算的。因为它们不是共享的,所以它们应该独立于享元对象。在我们的示例中,Character 类的 char 属性是外部状态。
- 内部状态:这些状态是在享元对象内部存储的,并且是共享的。它们独立于具体的场景并且不会随着场景的变化而改变。
享元模式通过分离内部和外部状态,使得系统可以更有效地共享对象,从而减少对象的数量和内存使用。
享元模式在 React 中的应用
让我们考虑一个在 React 中遇到的一个场景:一个网页上有许多工具提示 message。当用户将鼠标悬停在某些元素上时,工具提示会显示相关的信息。这些工具提示都具有相似的样式和动画,但显示的文本内容不同。
在没有使用享元模式的情况下,每当一个元素需要显示工具提示时,可能会为其创建一个新的 DOM 元素。但如果页面上有数百个这样的元素,这样做将会很低效。
使用享元模式的思想,我们可以只创建一个工具提示的 DOM 元素,并在需要时重用它,只是根据需要改变其内容和位置。
接下来情况具体代码实现:
jsx
const TooltipContext = React.createContext();
function TooltipProvider({ children }) {
const [tooltipData, setTooltipData] = React.useState(null);
const showTooltip = (content, position) => {
setTooltipData({ content, position });
};
const hideTooltip = () => {
setTooltipData(null);
};
return (
<TooltipContext.Provider value={showTooltip}>
{children}
{tooltipData && (
<Tooltip
content={tooltipData.content}
position={tooltipData.position}
/>
)}
</TooltipContext.Provider>
);
}
function Tooltip({ content, position }) {
const styles = {
position: "absolute",
top: position.top,
left: position.left,
/* other styles */
};
return <div style={styles}>{content}</div>;
}
function HoverableElement({ content }) {
const showTooltip = React.useContext(TooltipContext);
const handleMouseOver = (e) => {
const position = { top: e.clientY, left: e.clientX };
showTooltip(content, position);
};
return <div onMouseOver={handleMouseOver}>Hover me!</div>;
}
// 使用:
// <TooltipProvider>
// <HoverableElement content="Tooltip content here" />
// {/* ... other HoverableElements */}
// </TooltipProvider>
在上面的示例中,TooltipProvider 组件管理单个 Tooltip 的状态和位置。HoverableElement 组件在悬停时使用上下文来调用 showTooltip 函数,该函数会更新 Tooltip 的内容和位置。这样,无论页面上有多少 HoverableElement 组件,都只有一个 Tooltip DOM 元素,这就是享元模式的应用。
参考文献
- 书籍:JavaScript 设计模式与开发实践
总结
享元模式它通过将对象的共享部分(内部状态)与非共享部分(外部状态)分离,使得对象能够在多个上下文中共享。这种方法可以大大减少系统中对象的数量,从而减少内存占用和提高性能,尤其在处理大量类似对象时尤为有效。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🚗🚗🚗