JavaScript 享元(Flyweight)模式

2021-09-03 18:19 更新

享元模式

享元模式是一個優(yōu)化重復(fù)、緩慢和低效數(shù)據(jù)共享代碼的經(jīng)典結(jié)構(gòu)化解決方案。它的目標是以相關(guān)對象盡可能多的共享數(shù)據(jù),來減少應(yīng)用程序中內(nèi)存的使用(例如:應(yīng)用程序的配置、狀態(tài)等)。

此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳擊等級中少于112磅體重的等級名稱來命名。享元(“Flyweight”英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(內(nèi)存標記)為目標的重量等級推導(dǎo)出的。

實際應(yīng)用中,輕量級的數(shù)據(jù)共享采集被多個對象使用的相似對象或數(shù)據(jù)結(jié)構(gòu),并將這些數(shù)據(jù)放置于單個的擴展對象中。我們可以把它傳遞給依靠這些數(shù)據(jù)的對象,而不是在他們每個上面都存儲一次。

使用享元

有兩種方法來使用享元。第一種是數(shù)據(jù)層,基于存儲在內(nèi)存中的大量相同對象的數(shù)據(jù)共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程序關(guān)聯(lián)到我們需要相同行為父容器的所有子節(jié)點上。 享元模式通常被更多的用于數(shù)據(jù)層,我們先來看看它。

享元和數(shù)據(jù)共享

對于這個應(yīng)用程序而言,圍繞經(jīng)典的享元模式有更多需要我們意識到的概念。享元模式中有一個兩種狀態(tài)的概念——內(nèi)在和外在。內(nèi)在信息可能會被我們的對象中的內(nèi)部方法所需要,它們絕對不可以作為功能被帶出。外在信息則可以被移除或者放在外部存儲。

帶有相同內(nèi)在數(shù)據(jù)的對象可以被一個單獨的共享對象所代替,它通過一個工廠方法被創(chuàng)建出來。這允許我們?nèi)ワ@著降低隱式數(shù)據(jù)的存儲數(shù)量。

個中的好處是我們能夠留心于已經(jīng)被初始化的對象,讓只有不同于我們已經(jīng)擁有的對象的內(nèi)在狀態(tài)時,新的拷貝才會被創(chuàng)建。

我們使用一個管理器來處理外在狀態(tài)。如何實現(xiàn)可以有所不同,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態(tài)以及它們所屬的享元對象的中心數(shù)據(jù)庫。

經(jīng)典的享元實現(xiàn)

近幾年享元模式已經(jīng)在Javascript中得到了深入的應(yīng)用,我們會用到的許多實現(xiàn)方式其靈感來自于Java和C++的世界。

我們第一個要來看的關(guān)于享元模式的代碼是我的對來自維基百科的針對享元模式的 Java 示例的 Javascript 實現(xiàn)。

在這個實現(xiàn)中我們將要使用如下所列的三種類型的享元組件:

  • 享元對應(yīng)的是一個接口,通過此接口能夠接受和控制外在狀態(tài)。
  • 構(gòu)造享元來實際的實際的實現(xiàn)接口,并存儲內(nèi)在狀態(tài)。構(gòu)造享元須是能夠被共享的,并且具有操作外在狀態(tài)的能力。
  • 享元工廠負責(zé)管理享元對象,并且也創(chuàng)建它們。它確保了我們的享元對象是共享的,并且可以對其作為一組對象進行管理,這一組對象可以在我們需要的時候查詢其中的單個實體。如果一個對象已經(jīng)在一個組里面創(chuàng)建好了,那它就會返回該對象,否則它會在對象池中新創(chuàng)建一個,并且返回之。

這些對應(yīng)于我們實現(xiàn)中的如下定義:

  • CoffeeOrder:享元
  • CoffeeFlavor:構(gòu)造享元
  • CoffeeOrderContext:輔助器
  • CoffeeFlavorFactory:享元工廠
  • testFlyweight:對我們享元的使用

鴨式?jīng)_減的 “implements”

鴨式?jīng)_減允許我們擴展一種語言或者解決方法的能力,而不需要變更運行時的源。由于接下的方案需要使用一個Java關(guān)鍵字“implements”來實現(xiàn)接口,而在Javascript本地看不到這種方案,那就讓我們首先來對它進行鴨式?jīng)_減。

Function.prototype.implementsFor 在一個對象構(gòu)造器上面起作用,并且將接受一個父類(函數(shù)—)或者對象,而從繼承于普通的繼承(對于函數(shù)而言)或者虛擬繼承(對于對象而言)都可以。

// Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){ if ( parentClassOrObject.constructor === Function ) { // Normal Inheritance this.prototype = new parentClassOrObject(); this.prototype.constructor = this; this.prototype.parent = parentClassOrObject.prototype; } else { // Pure Virtual Inheritance this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject; } return this; };

我們可以通過讓一個函數(shù)明確的繼承自一個接口來彌補implements關(guān)鍵字的缺失。下面,為了使我們得以去分配支持一個對象的這些實現(xiàn)的功能,CoffeeFlavor實現(xiàn)了CoffeeOrder接口,并且必須包含其接口的方法。

// Flyweight object
var CoffeeOrder = {

  // Interfaces
  serveCoffee:function(context){},
    getFlavor:function(){}

};

// ConcreteFlyweight object that creates ConcreteFlyweight
// Implements CoffeeOrder
function CoffeeFlavor( newFlavor ){

    var flavor = newFlavor;

    // If an interface has been defined for a feature
    // implement the feature
    if( typeof this.getFlavor === "function" ){
      this.getFlavor = function() {
          return flavor;
      };
    }

    if( typeof this.serveCoffee === "function" ){
      this.serveCoffee = function( context ) {
        console.log("Serving Coffee flavor "
          + flavor
          + " to table number "
          + context.getTable());
    };     
    }

}

// Implement interface for CoffeeOrder
CoffeeFlavor.implementsFor( CoffeeOrder );

// Handle table numbers for a coffee order
function CoffeeOrderContext( tableNumber ) {
   return{
      getTable: function() {
         return tableNumber;
     }
   };
}

function CoffeeFlavorFactory() {
    var flavors = {},
    length = 0;

    return {
        getCoffeeFlavor: function (flavorName) {

            var flavor = flavors[flavorName];
            if (flavor === undefined) {
                flavor = new CoffeeFlavor(flavorName);
                flavors[flavorName] = flavor;
                length++;
            }
            return flavor;
        },

        getTotalCoffeeFlavorsMade: function () {
            return length;
        }
    };
}

// Sample usage:
// testFlyweight()

function testFlyweight(){

  // The flavors ordered.
  var flavors = new CoffeeFlavor(),

  // The tables for the orders.
    tables = new CoffeeOrderContext(),

  // Number of orders made
    ordersMade = 0,

  // The CoffeeFlavorFactory instance
    flavorFactory;

  function takeOrders( flavorIn, table) {
     flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn );
     tables[ordersMade++] = new CoffeeOrderContext( table );
  }

   flavorFactory = new CoffeeFlavorFactory();

   takeOrders("Cappuccino", 2);
   takeOrders("Cappuccino", 2);
   takeOrders("Frappe", 1);
   takeOrders("Frappe", 1);
   takeOrders("Xpresso", 1);
   takeOrders("Frappe", 897);
   takeOrders("Cappuccino", 97);
   takeOrders("Cappuccino", 97);
   takeOrders("Frappe", 3);
   takeOrders("Xpresso", 3);
   takeOrders("Cappuccino", 3);
   takeOrders("Xpresso", 96);
   takeOrders("Frappe", 552);
   takeOrders("Cappuccino", 121);
   takeOrders("Xpresso", 121);

   for (var i = 0; i < ordersMade; ++i) {
       flavors[i].serveCoffee(tables[i]);
   }
   console.log(" ");
   console.log("total CoffeeFlavor objects made: " +  flavorFactory.getTotalCoffeeFlavorsMade());
} <span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>

轉(zhuǎn)換代碼為使用享元模式

接下來,讓我們通過實現(xiàn)一個管理一個圖書館中所有書籍的系統(tǒng)來繼續(xù)觀察享元。分析得知每一本書的重要元數(shù)據(jù)如下:

  • ID
  • 標題
  • 作者
  • 類型
  • 總頁數(shù)
  • 出版商ID
  • ISBN

我們也將需要下面一些屬性,來跟蹤哪一個成員是被借出的一本特定的書,借出它們的日期,還有預(yù)計的歸還日期。

  • 借出日期
  • 借出的成員
  • 規(guī)定歸還時間
  • 可用性
var Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){

   this.id = id;
   this.title = title;
   this.author = author;
   this.genre = genre;
   this.pageCount = pageCount;
   this.publisherID = publisherID;
   this.ISBN = ISBN;
   this.checkoutDate = checkoutDate;
   this.checkoutMember = checkoutMember;
   this.dueReturnDate = dueReturnDate;
   this.availability = availability;

};

Book.prototype = {

  getTitle: function () {
     return this.title;
  },

  getAuthor: function () {
     return this.author;
  },

  getISBN: function (){
     return this.ISBN;
  },

  // For brevity, other getters are not shown
  updateCheckoutStatus: function( bookID, newStatus, checkoutDate , checkoutMember, newReturnDate ){

     this.id  = bookID;
     this.availability = newStatus;
     this.checkoutDate = checkoutDate;
     this.checkoutMember = checkoutMember;
     this.dueReturnDate = newReturnDate;

  },

  extendCheckoutPeriod: function( bookID, newReturnDate ){

      this.id =  bookID;
      this.dueReturnDate = newReturnDate;

  },

  isPastDue: function(bookID){

     var currentDate = new Date();
     return currentDate.getTime() > Date.parse( this.dueReturnDate );

   }
};

這對于最初小規(guī)模的藏書可能工作得還好,然而當圖書館擴充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,我們會發(fā)現(xiàn)管理系統(tǒng)的運行隨著時間的推移會越來越慢。使用成千上萬的書籍對象可能會壓倒內(nèi)存,而我們可以通過享元模式的提升來優(yōu)化我們的系統(tǒng)。

現(xiàn)在我們可以像下面這樣將我們的數(shù)據(jù)分離成為內(nèi)在和外在的狀態(tài):同書籍對象(標題,版權(quán)歸屬)相關(guān)的數(shù)據(jù)是內(nèi)在的,而借出數(shù)據(jù)(借出成員,規(guī)定歸還日期)則被看做是外在的。這實際上意味著對于每一種書籍屬性的組合僅需要一個書籍對象。這仍然具有相當大的數(shù)量,但相比之前已經(jīng)得到大大的縮減了。

下面的書籍元數(shù)據(jù)組合的單一實體將在所有帶有一個特定標題的書籍拷貝中共享。

// Flyweight optimized version
var Book = function ( title, author, genre, pageCount, publisherID, ISBN ) {

    this.title = title;
    this.author = author;
    this.genre = genre;
    this.pageCount = pageCount;
    this.publisherID = publisherID;
    this.ISBN = ISBN;

};

如我們所見,外在狀態(tài)已經(jīng)被移除了。從圖書館借出所要做的一切都被轉(zhuǎn)移到一個管理器中,由于對象數(shù)據(jù)現(xiàn)在是分段的,工廠可以被用來做實例化。

一個基本工廠

現(xiàn)在讓我們定義一個非?;镜墓S。我們用它做的工作是,執(zhí)行一個檢查來看看一本給定標題的書是不是之前已經(jīng)在系統(tǒng)內(nèi)創(chuàng)建過了;如果創(chuàng)建過了,我們就返回它 - 如果沒有,一本新書就會被創(chuàng)建并保存,使得以后可以訪問它。這確保了為每一條本質(zhì)上唯一的數(shù)據(jù),我們只創(chuàng)建了一份單一的拷貝:

// Book Factory singleton
var BookFactory = (function () {
  var existingBooks = {}, existingBook;

  return {
    createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {

      // Find out if a particular book meta-data combination has been created before
      // !! or (bang bang) forces a boolean to be returned
      existingBook = existingBooks[ISBN];
      if ( !!existingBook ) {
        return existingBook;
      } else {

        // if not, let's create a new instance of the book and store it
        var book = new Book( title, author, genre, pageCount, publisherID, ISBN );
        existingBooks[ISBN] = book;
        return book;

      }
    }
  };

});

管理外在狀態(tài)

下一步,我們需要將那些從Book對象中移除的狀態(tài)存儲到某一個地方——幸運的是一個管理器(我們會將其定義成一個單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。這些我們的管理器都將會存儲,并且也包含我們在對Book類進行享元優(yōu)化期間剝離的同借出相關(guān)的邏輯。

// BookRecordManager singleton
var BookRecordManager = (function () {

  var bookRecordDatabase = {};

  return {
    // add a new book into the library system
    addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {

      var book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );

      bookRecordDatabase[id] = {
        checkoutMember: checkoutMember,
        checkoutDate: checkoutDate,
        dueReturnDate: dueReturnDate,
        availability: availability,
        book: book
      };
    },
    updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {

      var record = bookRecordDatabase[bookID];
      record.availability = newStatus;
      record.checkoutDate = checkoutDate;
      record.checkoutMember = checkoutMember;
      record.dueReturnDate = newReturnDate;
    },

    extendCheckoutPeriod: function ( bookID, newReturnDate ) {
      bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
    },

    isPastDue: function ( bookID ) {
      var currentDate = new Date();
      return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );
    }
  };

});

這些改變的結(jié)果是所有從Book類中擷取的數(shù)據(jù)現(xiàn)在被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與我們以前使用大量對象相比可以被認為是更加高效的東西。同書籍借出相關(guān)的方法也被設(shè)置在這里,因為它們處理的數(shù)據(jù)是外在的而不內(nèi)在的。

這個過程確實給我們最終的解決方法增加了一點點復(fù)雜性,然而同已經(jīng)明智解決的數(shù)據(jù)性能問題相比,這只是一個小擔(dān)憂,如果我們有同一本書的30份拷貝,現(xiàn)在我們只需要存儲它一次就夠了。每一個函數(shù)也會占用內(nèi)存。使用享元模式這些函數(shù)只在一個地方存在(就是在管理器上),并且不是在每一個對象上面,這節(jié)約了內(nèi)存上的使用。

享元模式和DOM

DOM(文檔對象模型)支持兩種允許對象偵聽事件的方法——自頂向下(事件捕獲)或者自底向下(時間冒泡)。 在事件捕獲中,事件一開始會被最外面的元素捕獲,并且傳播到最里面的元素。在事件冒泡中,事件被捕獲并且被賦給了最里面的元素,然后傳播到最外面的元素。

在此背景下描述享元模式的最好隱喻來自Gary Chisholm寫的文章,這里摘錄了一點點:

嘗試用一種池塘的方式思考享元模式。一只魚張開了它的嘴巴(事件發(fā)生了),泡泡一直要上升到表面(冒泡),當泡泡到達表面時,停泊在頂部的一直蒼蠅飛走了(動作執(zhí)行)。在這個示例中我們能夠很容易的將魚張開嘴巴轉(zhuǎn)換為按鈕被點擊了一下,將泡泡轉(zhuǎn)換為冒泡效果,而蒼蠅飛走了表示一些需要運行的函數(shù)。

冒泡被引入用來處理單個事件(比如:一次點擊)可能會由在DOM層級中的不同級別的多個事件處理器處理,這樣的場景。這在哪里發(fā)生了,事件冒泡就會為在盡可能最低的級別定義的事件處理器執(zhí)行。從那里開始,事件向上冒泡,一直到包含比應(yīng)該包含的更高層級的元素。

享元模式可用來進一步調(diào)整事件冒泡過程,這我們很快就將會看到。

例子1:集中式事件處理

一起來看看我們第一例子,當用戶有個動作(如點擊或是鼠標移動)時我們將有很多相似的文檔對象以及相似的行為要處理。一般情況下,當我們構(gòu)建手風(fēng)琴式控件,菜單以及其它列表控件時,就會在每一個超鏈接元素父容器里綁定點擊事件(如,$('ul li a').on(..)(jQuery代碼,譯者注))。我們可以方便的在可以監(jiān)聽事件容器里添加Flyweight,而不是在很多元素里綁定點擊事件。這樣就可處理或是簡單或是復(fù)雜的需求。

提到組件的類型,經(jīng)常會涉及到很多部分都有同樣重復(fù)的標簽(如,手風(fēng)琴式控件),這是個好機會,每個元素都有可能被點擊的行為,而且基本上用相同的類。我們可以用Flyweight來構(gòu)建一個基本的手風(fēng)琴控件。

這里我們使用一個stateManager命名空間來封裝我們的享元邏輯,同時使用jQuery來把初始點擊事件綁定到一個div容器上。為了確保頁面上沒有其他程序邏輯把類似的處理器綁定到該容器上,首先使用了一個unbind事件。

現(xiàn)在明確的確立一下容器中的那個子元素會被點擊,我們使用一次對target的檢查來提供對被點擊元素的引用,而不管它的父元素是誰。然后我們利用該信息來處理點擊事件,而實際上不需要在頁面裝載時把該事件綁定到具體的子元素上。

HTML

<div id="container">
   <div class="toggle" href="#">More Info (Address)
       <span class="info">
           This is more information
       </span></div>
   <div class="toggle" href="#">Even More Info (Map)
       <span class="info">
          <iframe src="https://atts.w3cschool.cn/attachments/image/cimg/extmap.php?name=London&address=london%2C%20england&amp;width=500...gt;"</iframe>
       </span>
   </div>
</div>

JavaScript

var stateManager = {

  fly: function () {

    var self = this;

    $( "#container" ).unbind().on( "click" , function ( e ) {
      var target = $( e.originalTarget || e.srcElement );
        if ( target.is( "div.toggle") ) {
          self.handleClick( target );
        }
    });
  },

  handleClick: function ( elem ) {
    elem.find( "span" ).toggle( "slow" );
  }
};

這樣做的好處是,我們把許多不相關(guān)的動作轉(zhuǎn)換為一個可以共享的動作(也許會保存在內(nèi)存中)。

示例2:使用享元進行性能優(yōu)化

在我們的第二個示例中,我們將會引述通過使用jQuery的享元可以獲得的一些更多的性能上的收獲。

Jame Padolsey 以前寫過一篇叫做76比特的文章,講述更快的jQuery,在其中他提醒我們每一次jQuery觸發(fā)了一個回調(diào),不管是什么類型(過濾器,每一個,事件處理器),我們都能夠通過this關(guān)鍵字訪問函數(shù)的上下文(與它相關(guān)的DOM元素)。

不幸的是,我們中的許多人已經(jīng)習(xí)慣將this封裝到$()或者jQuery()中的想法,這意味著新的jQuery實體沒必要每次都被構(gòu)造出來,而是簡單的這樣做:

$("div").on( "click", function () {
  console.log( "You clicked: " + $( this ).attr( "id" ));
});

// we should avoid using the DOM element to create a
// jQuery object (with the overhead that comes with it)
// and just use the DOM element itself like this:

$( "div" ).on( "click", function () {
  console.log( "You clicked:"  + this.id );
});

James想要下面的場景中使用jQuery的jQuery.text,然而他不能茍同一個新的jQuery對象必須在每次迭代中創(chuàng)建的概念。

$( "a" ).map( function () {
  return $( this ).text();
});

現(xiàn)在就使用jQuery的工具方法進行多余的包裝而言,使用jQuery.methodName(如,jQuery.text)比jQuery.fn.methodName(如,jQuery.fn.text)更好,這里methodName代表了一種使用的工具,如each()或者text。這避免了調(diào)用更深遠級別的抽象,或者每一次當我們的函數(shù)被調(diào)用時就構(gòu)造一個新的jQuery對象,因為定義了jQuery.methodName的庫本身在更底層使用jQuery.fn.methodName驅(qū)動的。

然而由于并不是所有jQuery的方法都有相應(yīng)的單節(jié)點功能,Padolsey根據(jù)這個創(chuàng)意設(shè)計了jQuery.single工具。 這里的創(chuàng)意是一個單獨的jQuery對象會被被創(chuàng)建出來并且用于每一次對jQuery.single的調(diào)用(有意義的是僅有一個jQuery對象會被創(chuàng)建出來)。對于此的實現(xiàn)可以在下面看到,而且由于我們將來自多個可能的對象的數(shù)據(jù)整合到一個更加集中的單一結(jié)構(gòu)中,技術(shù)上講,它也是一個享元。

jQuery.single = (function( o ){

   var collection = jQuery([1]);
   return function( element ) {

       // Give collection the element:
       collection[0] = element;

        // Return the collection:
       return collection;

   };
});

對于這個的帶有調(diào)用鏈的動作的示例如下:

$( "div" ).on( "click", function () {

   var html = jQuery.single( this ).next().html();
   console.log( html );

});

注意:盡管我們可能相信通過簡單的緩存我們的jQuery代碼會提供出同等良好的性能收獲,但Padolsey聲稱$.single()仍然值得使用,且表現(xiàn)更好。那并不是說不使用任何的緩存,只要對這種方法的助益做到心里有數(shù)就行。想要對$.single有更加詳細的了解,建議你卻讀一讀Padolsey完整的文章。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號