JavaScript设计模式与开发实践 享元模式

  享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

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

    一、内部状态与外部状态

  享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。

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

  剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态。而外部状态取决于具体的场景,并根据场景而变化,就像例子中每件衣服都是不同的,它们不能被一些对象共享,因此只能被划分为外部状态。

    二、享元模式的通用结构

   2.1文件上传的例子

    如果每上传一个文件都创建一个对象,创建过多对象会让浏览器进入假死状态。    

    当用户选择了文件之后,插件和Flash 都会通知调用Window 下的一个全局JavaScript 函数,它的名字是startUpload,用户选择的文件列表被组合成一个数组files 塞进该函数的参数列表里:

    var id = 0;
    window.startUpload = function( uploadType, files ){ // uploadType 区分是控件还是flash
        for ( var i = 0, file; file = files[ i++ ]; ){
            var uploadObj = new Upload( uploadType, file.fileName, file.fileSize );
            uploadObj.init( id++ ); // 给upload 对象设置一个唯一的id
        }
    };

    var Upload = function( uploadType, fileName, fileSize ){
        this.uploadType = uploadType;
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.dom= null;
    };
    Upload.prototype.init = function( id ){
        var that = this;
        this.id = id;
        this.dom = document.createElement( 'div' );
        this.dom.innerHTML =
        '<span>文件名称:'+ this.fileName +', 文件大小: '+ this.fileSize +'</span>' +
        '<button class="delFile">删除</button>';
        this.dom.querySelector( '.delFile' ).onclick = function(){
            that.delFile();
        }
        document.body.appendChild( this.dom );
    };

    Upload.prototype.delFile = function(){
        if ( this.fileSize < 3000 ){
            return this.dom.parentNode.removeChild( this.dom );
        }
        if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
            return this.dom.parentNode.removeChild( this.dom );
        }
    };

    startUpload( 'plugin', [
        {
            fileName: '1.txt',
            fileSize: 1000
        },
        {
            fileName: '2.html',
            fileSize: 3000
        },
        {
            fileName: '3.txt',
            fileSize: 5000
        }
    ]);
    startUpload( 'flash', [
        {
            fileName: '4.txt',
            fileSize: 1000
        },
        {
            fileName: '5.html',
            fileSize: 3000
        },
        {
            fileName: '6.txt',
            fileSize: 5000
        }
    ]); 

  享元模式重构文件上传

  插件类型uploadType 是内部状态在文件上传的例子里,upload 对象必须依赖uploadType 属性才能工作,这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,必须在对象创建之初就明确它是什么类型的插件,才可以在程序的运行过程中,让它们分别调用各自的start、pause、cancel、del 等方法。

  一旦明确了uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的。而fileName 和fileSize 是根据场景而变化的,每个文件的fileName 和fileSize 都不一样,fileName 和fileSize 没有办法被共享,它们只能被划分为外部状态。

  Upload.prototype.init 函数也不再需要,因为upload 对象初始化的工作被放在了upload-Manager.add 函数里面,接下来只需要定义Upload.prototype.del 函数即可。

x

    var Upload = function( uploadType){
        this.uploadType = uploadType;
    };

//
定义一个工厂来创建upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象
    Upload.prototype.delFile = function( id ){
        uploadManager.setExternalState( id, this ); // (1)
        if ( this.fileSize < 3000 ){
            return this.dom.parentNode.removeChild( this.dom );
        }

        if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
            return this.dom.parentNode.removeChild( this.dom );
        }
    }

    //定义一个工厂来创建upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象
var UploadFactory = (function(){
        var createdFlyWeightObjs = {};
        return {
            create: function( uploadType){
                if ( createdFlyWeightObjs [ uploadType] ){
                    return createdFlyWeightObjs [ uploadType];
                }
                return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
            }
        }
    })();
    
//
完善前面提到的uploadManager 对象,它负责向UploadFactory 提交创建对象的请求,并用一个uploadDatabase 对象保存所有upload 对象的外部状态,以便在程序运行过程中给upload 共享对象设置外部状态
var uploadManager = (function(){
        var uploadDatabase = {};
        return {
            add: function( id, uploadType, fileName, fileSize ){
                var flyWeightObj = UploadFactory.create( uploadType );
                var dom = document.createElement( 'div' );
                dom.innerHTML =
                '<span>文件名称:'+ fileName +', 文件大小: '+ fileSize +'</span>' +
                '<button class="delFile">删除</button>';
                dom.querySelector( '.delFile' ).onclick = function(){
                    flyWeightObj.delFile( id );
                }

                document.body.appendChild( dom );
                uploadDatabase[ id ] = {
                    fileName: fileName,
                    fileSize: fileSize,
                    dom: dom
                };
                return flyWeightObj ;
            },
            setExternalState: function( id, flyWeightObj ){
                var uploadData = uploadDatabase[ id ];
                for ( var i in uploadData ){
                    flyWeightObj[ i ] = uploadData[ i ];
                }
            }
        }
    })();

    var id = 0;
    window.startUpload = function( uploadType, files ){
        for ( var i = 0, file; file = files[ i++ ]; ){
            var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
        }
    };


    startUpload( 'plugin', [
        {
            fileName: '1.txt',
            fileSize: 1000
        },
        {
            fileName: '2.html',
            fileSize: 3000
        },
        {
            fileName: '3.txt',
            fileSize: 5000
        }
    ]);
    startUpload( 'flash', [
        {
            fileName: '4.txt',
            fileSize: 1000
        },
        {
            fileName: '5.html',
            fileSize: 3000
        },
        {
            fileName: '6.txt',
            fileSize: 5000
        }
    ]);

 

  享元模式重构之前的代码里一共创建了6 个upload 对象,而通过享元模式重构之后,对象的数量减少为2,就算现在同时上传2000 个文件,需要创建的upload 对象数量依然是2。

  2.2享元模式的适用性

  使用了享元模式之后,我们需要分别多维护一个factory 对象和一个manager 对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。

  一般来说,以下情况发生时便可以使用享元模式。

  • 一个程序中使用了大量的相似对象。
  • 由于使用了大量对象,造成很大的内存开销。
  • 对象的大多数状态都可以变为外部状态。
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

    三、再谈内部状态和外部状态

  实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象。现在来考虑两种极端的情况,即对象没有外部状态和没有内部状态的时候。

  3.1没有内部状态的享元

  3.2没有内部状态的享元

    四、对象池

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

  对象池技术的应用非常广泛,HTTP 连接池和数据库连接池都是其代表应用。在Web 前端开发中,对象池使用最多的场景大概就是跟DOM 有关的操作。很多空间和时间都消耗在了DOM节点上,如何避免频繁地创建和删除DOM 节点就成了一个有意义的话题。

      4.1 对象池实现

  在开发一个地图应用, 地图上经常会出现一些标志地名的小气泡,我们叫它toolTip。假如第一次搜索后页面出现了2个小气泡,第二次搜索出现了6个小气泡,按照对象池的思想,在第二次搜索开始之前,并不会把第一次创建的2 个小气泡删除掉,而是把它们放进对象池。这样在第二次的搜索结果页面里,我们只需要再创建4 个小气泡而不是6 个

 

    //定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包里,
  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 ary = [];
    for ( var i = 0, str; str = [ 'A', 'B' ][ i++ ]; ){
        var toolTip = toolTipFactory.create();
        toolTip.innerHTML = str;
        ary.push( toolTip );
    };

  //假设地图需要开始重新绘制,在此之前要把这两个节点回收进对象池
    for ( var i = 0, toolTip; toolTip = ary[ i++ ]; ){
        toolTipFactory.recover( toolTip );
    };

 //创建6 个小气泡
    for ( var i = 0, str; str = [ 'A', 'B', 'C', 'D', 'E', 'F' ][ i++ ]; ){
        var toolTip = toolTipFactory.create();
        toolTip.innerHTML = str;
    };

 

  4.2 通用对象池实现

  我们还可以在对象池工厂里,把创建对象的具体过程封装起来,实现一个通用的对象池:

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

  对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。本章用享元模式完成了一个文件上传的程序,其实也可以用对象池+事件委托来代替实现。

    五、小结

  享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。

 

posted on 2016-11-23 11:46  Surahe  阅读(262)  评论(0编辑  收藏  举报