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

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

        五、小结

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

  • 相关阅读:
    理解RESTful架构
    redis 未授权漏洞利用直接登录服务器
    初创公司应该如何做好持续集成和部署?
    Redis 作为缓存服务器的配置
    自己写的轻量级PHP框架trig与laravel5.1,yii2性能对比
    利用SecureCRT上传、下载文件(使用sz与rz命令)
    ZendStudio10 代码格式化 xml
    LESS CSS 框架简介
    为什么浏览器User-agent总是有Mozilla字样
    在 JavaScript 中 prototype 和 __proto__ 有什么区别
  • 原文地址:https://www.cnblogs.com/surahe/p/6085630.html
Copyright © 2020-2023  润新知