享元模式

2016-10-12 曹强 更多博文 » 博客 » GitHub »

javascript

原文链接 https://ronghuaxueleng.github.io/2016/10/12/JavaScript%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B8%8E%E5%BC%80%E5%8F%91%E5%AE%9E%E8%B7%B5-14%E4%BA%AB%E5%85%83%E6%A8%A1%E5%BC%8F/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。



享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在JavaScript中,浏览器特别是移动端的浏览器分配的内存不算多,如何节省内存就成了一件非常有意义的事。

初识

假设有个内衣工厂,要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();
}

要得到一张照片,每次都需要传入sexunderwear参数,根据现在的需求一共会产生100个对象。但是如果将来要生产10000种,那这个程序可能会因为存在太多对象提前崩溃。所以我们做一点改动

var Model = function(sex) {
    this.sex = sex;
}
Model.pototype.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();
}

以上就是享元模式的雏形,享元模式要求将对象的属性划分为内部状态与外部状态。享元模式的目标是尽量减少共享对象的数量。关于如何划分内部状态和外部状态,可参考以下 :

  • 内部状态存储于内部对象

  • 内部状态可以做一些对象共享

  • 内部状态独立于具体的场景,通常不会改变

  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享

在上面例子中,性别是内部状态,内衣是外部状态。通常来说内部状态有多少种组合系统中便最多存在多少对象。
但是,以上还不是一个完整的享元模式,在这个例子中还有两个问题

  • 我们通过构造函数显示的new出了男女两个model对象,在其他系统中也许并不是一开始就需要所有的共享对象

  • model对象手动设置了underwear外部状态,在更复杂的系统中这不是一个最好的方式因为外部状态可能相对复杂,他与共享对象的联系会变得困难。

我们通过第一个对象工厂来解决第一个问题,只有某种共享对象被真正需要时,他才从工厂中被创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

对象池

在java中,对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成他/它的职责之后,再进入池子等待下次获取。

这里稍微介绍一下对象池工厂里实现通用对象池的方法:

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);
        }
    }
};

现在利用objectPoolFactory来创建一个装载一些iframe的对象池:

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);

对象池是另外一种性能优化方案,他跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。