前言

享元模式是一种性能优化的模式,它通过共享对象的方式,减少实例对象的数量,它将对象的属性拆分为内部属性和外部属性,内部属性是可以共享的,外部属性是无法共享的,我们将共享的部分封装到对象中,然后大家共用这一个对象,通过方法或者直接赋值外部属性给这个对象,从而实现对象的复用。

但是赋值的过程是会耗时的,这就是典型的时间换空间的优化方式了。

享元模式是一种结构型设计模式,旨在减少应用程序中的内存使用或计算开销,通过共享对象实例来优化性能。

初识享元模式

假设有个内衣工厂,目前的产品有 50 种男式内衣和 50 种女士内衣,为了推销产品,工厂决 定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50 个男模特和 50 个女 模特,然后让他们每人分别穿上一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:

var Model = function(sex, underwear) {
    this.sex = sex;
    this.underwear = underwear;
};

Model.prototype.takePhoto = function() {
    console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};

for (var i = 1; i <= 50; i++) {
    var maleModel = new Model('male', 'underwear' + i);
    maleModel.takePhoto();
};

for (var j = 1; j <= 50; j++) {
    var femaleModel = new Model('female', 'underwear' + j);
    femaleModel.takePhoto();
};

想要得到一张照片,就得new一个新的model对象实例,如果我们将来需要100000个照片,就得new出这么多对象,显然会产生性能上的问题。

那么如何优化这个场景,显然我们不需要那么多模特对象,其实男女模特只需要各一个就行了,然后只需要让他们各自穿不同的衣服拍照即可。

改造一下代码:

var Model = function(sex) {
    this.sex = sex;
};

Model.prototype.takePhoto = function() {
    console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};

var maleModel = new Model('male'),
    femaleModel = new Model('female');

for (var i = 1; i <= 50; i++) {
    maleModel.underwear = 'underwear' + i;
    maleModel.takePhoto();
};

for (var j = 1; j <= 50; j++) {
    femaleModel.underwear = 'underwear' + j;
    femaleModel.takePhoto();
};

可以看到,现在我们只需要2个实例对象就完成了该功能。

内部状态和外部状态

享元模式将对象的属性分为内部状态和外部状态,那么如何区分内部和外部:

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

说白了就是将不变与变的部分拆分出来,不变的放在共享对象中,变的作为外部状态,在拿到共享对象后进行赋值。

文件上传的例子

// 文件享元工厂
const FileFlyweightFactory = (function() {
    const files = {}; // 保存已创建的文件享元实例

    return {
        getFile: function(name, size, type) {
            const fileKey = name + size + type; // 创建文件实例的唯一标识
            if (!files[fileKey]) {
                files[fileKey] = {
                    name,
                    size,
                    type,
                    uploaded: false
                }; 
            }
            return files[fileKey];
        }
    };
})();

// 示例代码
const file1 = FileFlyweightFactory.getFile("text.txt", 1024, "text/plain");
const file2 = FileFlyweightFactory.getFile("text.txt", 1024, "text/plain"); // 重复的文件

console.log(file1); // { name: 'text.txt', size: 1024, type: 'text/plain', uploaded: false }
console.log(file2); // { name: 'text.txt', size: 1024, type: 'text/plain', uploaded: false }

当我们在实现文件上传的时候,一般都需要创建一个用于上传的对象,这个对象假设就包含了name、size、type、uploaded,其中uploaded是固定的,所以它是内部属性,其他的是外部属性。

并且通过工厂方法,可以便捷的创建共享对象和获取到可复用的对象。

但是享元模式也有一个很大的弊端,从代码中我们可以看到,它只能适用于单线程的逻辑,如果我先要并行获取到共享对象并使用它,就会导致其他地方发生问题,它必须是一步一步来,用完了才可以给下一个人使用。

对象池

对象池和享元模式不是同一个东西,但是他们都实现了对象的复用,但是对象池更加侧重于管理可复用的对象实例(创建和销毁),而享元模式更关注需要大量共享对象实例的场景。

var toolTipFactory = (function() {
    var toolTipPool = []; // toolTip 对象池
    return {
        create: function() {
            if (toolTipPool.length === 0) { // 如果对象池为空
                var div = document.createElement('div'); // 创建一个 dom 
                document.body.appendChild(div);
                return div;
            } else { // 如果对象池里不为空
                return toolTipPool.shift(); // 则从对象池中取出一个 dom 
            }
        },
        recover: function(tooltipDom) {
            return toolTipPool.push(tooltipDom); // 对象池回收 dom 
        }
    }
})();

可以看到,对象池是支持并行的,相对于传统的享元模式,它更加适合需要大量实例的情况,且不关心实例什么时候使用完毕,不够直接创建即可。

它的应用场景在游戏中较多,比如飞机大战的子弹,如果使用享元模式,你需要等待子弹发射到屏幕外才能再次复用,显然不符合使用场景,使用对象池就可以快速创建多个子弹,在子弹超出屏幕外后再回收回来复用。

通用对象池

var objectPoolFactory = function(createObjFn) {
    var objectPool = [];
    return {
        create: function() {
            var obj = objectPool.length === 0 ?
                createObjFn.apply(this, arguments) : objectPool.shift();
            return obj;
        },
        recover: function(obj) {
            objectPool.push(obj);
        }
    }
};

var iframeFactory = objectPoolFactory(function() {
    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    iframe.onload = function() {
        iframe.onload = null; // 防止 iframe 重复加载的 bug 
        iframeFactory.recover(iframe); // iframe 加载完成之后回收节点
    }
    return iframe;
});

var iframe1 = iframeFactory.create();

iframe1.src = 'http:// baidu.com';

var iframe2 = iframeFactory.create();

iframe2.src = 'http:// QQ.com';

setTimeout(function() {
    var iframe3 = iframeFactory.create();
    iframe3.src = 'http:// 163.com';
}, 3000);

组合使用

// 创建对象池工厂
const ObjectPoolFactory = (function() {
    const pool = [];

    return {
        createObject: function(key) {
            const object = {
                key,
                inUse: false,
                operation() {
                    console.log(`Performing operation for key: ${key}`);
                }
            };
            pool.push(object);
            return object;
        },
        getAvailableObject: function() {
            return pool.find(obj => !obj.inUse);
        },
        recover(obj) {
          pool.push(obj);
        }

    };
})();

// 使用对象池工厂支持并行操作
const objects = [];
for (let i = 0; i < 10; i++) {
    const object = ObjectPoolFactory.createObject(`key${i}`);
    objects.push(object);
}

objects.forEach((object, index) => {
    setTimeout(() => {
        object.inUse = true;
        object.operation();
        object.inUse = false;
        ObjectPoolFactory.recover(obj);
    }, index * 1000);
});

我们可以将享元模式与对象池组合使用,以适应更加复杂的业务场景。

分类: JavaScript设计模式与开发实践 标签: JavaScript模式享元模式

评论

暂无评论数据

暂无评论数据

目录