Web前端框架 Backbone.Marionette

海興發表於2013-02-22

在Playframework樣例程式zentasks下的app\assets\javascripts\main.coffee中,有這樣一段話:

DISCLAMER :
If you're used to Backbone.js, you may be
confused by the absence of models, but the goal
of this sample is to demonstrate some features
of Play including the template engine.
I'm not using client-side templating nor models
for this purpose, and I do not recommend this
behavior for real life projects.

我斷定Backbone.js是個客戶端模板框架,因此我知道攤上事兒了,攤上大事兒了!便隨手把zentasks丟在地上,循著兔毛去追兔子。一路上不斷看到這隻兔子的各種傳說,撩撥的我心癢難耐,魂不守舍!因此在老婆大人訓話期間走神發呆,致使上峰震怒,被罰關小黑屋 :(

好在皇天不負有心人!幾經輾轉,終於讓我找到祕笈:Developing Backbone.js Applications。還有一篇介紹將Backbone.Marionette 用做Play 前端架構的文章:An advanced front-end architecture for Play! 2.0 with Backbone.js, Marionette, & CoffeeScript. 趕緊揀緊要的摘錄在此,以備後用。

BTW,Marionette = 牽線木偶,這是個寶貝。

Backbone.Marionette

Backbone.js 一夜間紅遍大江南北,以迅雷不及斷網之勢拿下模組化Javascript前端應用框架頭牌的位置。很大程度上得益於她良好的職業素養,見誰都是三分笑,一點不端架子,任誰都能招呼。雖然她天資聰穎,但畢竟出身於類庫之間,缺乏架構氣質,想讓她應付各種複雜局面,客官對不起,自己想轍砌。

所以對於想用Backbone.js做高階貨的玩家,入手Backbone.Marionette再合適不過了。人作者說了:“make[s] your Backbone.js apps dance with a composite application architecture!”看到沒,組合式應用架構,還輕舞飛揚,擎好吧您吶!

Backbone給Javascript準備了很多構件。有組織jQuery DOM事件,打造支援移動裝置以及大型企業應用的核心構造,但在應用設計,架構和擴充套件能力方面,開發人員得到的支援有限。

Backbone.Marionette (就叫 "Marionette" 吧) 站在了Backbone的肩膀上,給我們帶來了很多開發正經玩意兒的特性。它是為簡化大型應用構造工作而生的組合應用類庫。其中包含了一系列通用的Backbone應用設計和實現模式,是其建立者 Derick Bailey和其他 同仁 構建Backbone應用實戰經驗的結晶。

Marionette的核心價值在於:

  • 模組化,事件驅動的架構
  • 合理的預設配置,比如用Underscore 模板做檢視呈現
  • 易於根據特定需求進行修改
  • 提供特定的view型別,減少view呈現的套路化程式碼
  • Application及附著在其上的模組實現模組化架構
  • 藉助 RegionLayout,在執行時組合應用的顯示效果
  • 在可見區域內的巢狀式檢視和佈局
  • 內建的記憶體管理功能,可以殺死 views, regions 和 layouts 中的殭屍
  • 內建的EventBinder 事件清除
  • 藉助EventAggregator實現的事件驅動架構
  • 靈活, "用啥裝啥" 架構
  • 木木的好處數不清。。。

Marionette的哲學思想師承 Backbone,它提供的元件也是那種即可獨立使用互不干擾,又可相互合作共同發力。但它沒像Backbone那樣停留在結構化的元件上,而是在應用層面上提供了很多元件(component)和構件(building block)。好吧,這有點像文字遊戲,按我目前的認識,其實老外也就隨便一說。

Marionette的元件用途很廣,但它們能合在一起形成一個組合式應用層,既可減少套路化程式碼,也能提供更合理的應用結構。其核心元件包括:

但Marionette的元件也是按需取用,畢竟是Backbone家的,三觀一致啊。跟其它Backbone框架、外掛相處也很容易,其樂融融。升級的時候,也可以按元件來,不用眉毛鬍子一把抓。

Marionette App:Todo

Marionette 的核心優勢在前面都介紹過了,但實踐是檢驗真理的唯一標準,不寫個真正的程式,怎麼能體會到一個框架的好。接下來這個叫做Todo的程式,就是Marionette的試金石。所有的程式碼都在 Derick的 TodoMVC上,如果你願意,可以去github上 fork它。Developing Backbone.js Applications上還有用純粹Backbone實現的講解

一個完整的Marionette程式,一般由以下幾個部分組成:

  • application物件,其中會有初始化程式碼,以及預設佈局區域的定義。
  • 佈局定義,佈局是一種特殊的view,是Marionette.ItemView 的直接子型別。
  • 路由和工作流 -Controller
  • view,常見的是CompositeView和ItemView
  • Model 和 Collection

接下來,我們就先看看這個程式的大廳,application物件。

CompositeView

終於可以為單個的Todo項和Todo列表定義view了。為此,我們要用CompositeView,它是用來表示一個樹狀的組合或層級結構的視覺化元件。

你可以把這些views當做具有父子關係的層級結構,並且預設是可遞迴的。在組成view的item集合中,每個item都用CompositeView 型別的物件渲染。對於非遞迴的層級,我們可以定義一個 itemView屬性來覆蓋item 的 view。

對於我們的Todo列表而言,我們要把Item View定義為 ItemView,而List View定義為CompositeView,並覆蓋它的itemView設定,告訴它用ItemView表示集合中的每個item。

TodoMVC.TodoList.Views.js

TodoMVC.module('TodoList.Views', function(Views, App, Backbone, Marionette, $, _){

  // Todo List Item View
  // -------------------
  //
  // Display an individual todo item, and respond to changes
  // that are made to the item, including marking completed.

  Views.ItemView = Marionette.ItemView.extend({
    tagName : 'li',
      template : '#template-todoItemView',

      ui : {
        edit : '.edit'
      },

      events : {
        'click .destroy' : 'destroy',
        'dblclick label' : 'onEditClick',
        'keypress .edit' : 'onEditKeypress',
        'click .toggle'  : 'toggle'
      },

      initialize : function() {
        this.bindTo(this.model, 'change', this.render, this);
      },

      onRender : function() {
        this.$el.removeClass('active completed');
        if (this.model.get('completed')) this.$el.addClass('completed');
        else this.$el.addClass('active');
      },

      destroy : function() {
        this.model.destroy();
      },

      toggle  : function() {
        this.model.toggle().save();
      },

      onEditClick : function() {
        this.$el.addClass('editing');
        this.ui.edit.focus();
      },

      onEditKeypress : function(evt) {
        var ENTER_KEY = 13;
        var todoText = this.ui.edit.val().trim();

        if ( evt.which === ENTER_KEY && todoText ) {
          this.model.set('title', todoText).save();
          this.$el.removeClass('editing');
        }
      }
  });

  // Item List View
  // --------------
  //
  // Controls the rendering of the list of items, including the
  // filtering of active vs completed items for display.

  Views.ListView = Marionette.CompositeView.extend({
    template : '#template-todoListCompositeView',
      itemView : Views.ItemView,
      itemViewContainer : '#todo-list',

      ui : {
        toggle : '#toggle-all'
      },

      events : {
        'click #toggle-all' : 'onToggleAllClick'
      },

      initialize : function() {
        this.bindTo(this.collection, 'all', this.update, this);
      },

      onRender : function() {
        this.update();
      },

      update : function() {
        function reduceCompleted(left, right) { return left && right.get('completed'); }
        var allCompleted = this.collection.reduce(reduceCompleted,true);
        this.ui.toggle.prop('checked', allCompleted);

        if (this.collection.length === 0) {
          this.$el.parent().hide();
        } else {
          this.$el.parent().show();
        }
      },

      onToggleAllClick : function(evt) {
        var isChecked = evt.currentTarget.checked;
        this.collection.each(function(todo){
          todo.save({'completed': isChecked});
        });
      }
  });

  // Application Event Handlers
  // --------------------------
  //
  // Handler for filtering the list of items by showing and
  // hiding through the use of various CSS classes

  App.vent.on('todoList:filter',function(filter) {
    filter = filter || 'all';
    $('#todoapp').attr('class', 'filter-' + filter);
  });

});

在最後一塊程式碼中,你應該能注意到有個事件處理器用的是 vent。這是一個事件聚合器,我們可以用它處理 來自 TodoList 控制器的 filterItem 觸發器。

主廳 TodoMVC

我們要看下整個應用的主入口TodoMVC,不廢話,上程式碼:

TodoMVC.js:

var TodoMVC = new Marionette.Application();

TodoMVC.addRegions({
  header : '#header',
  main   : '#main',
  footer : '#footer'
});

TodoMVC.on('initialize:after', function(){
  Backbone.history.start();
});

區域是用來管理在特定元素中所顯示的內容的, 如果把TodoMVC當做主廳,那各個區域就可以算做不同房間。TodoMVC 物件中的addRegions 方法,是建立Region 物件的快捷方式。我們給每個region提供了一個jQuery選擇器(即 #header, #main#footer) ,指明它要管理的元素,然後告訴region在那個元素內顯示各種Backbon view。

在application物件完成初始化之後,我們馬上就呼叫Backbone.history.start() 轉向初始路由。

接下來,我們要定義佈局。佈局是特殊的view,直接擴充套件自Marionette.ItemView。也就是說我們要用它渲染某個模板,這個模板可能有關聯的model(或 item ),也可能沒有。

裝修設計圖 TodoMVC.Layout

LayoutItemView有個很大的區別,它可以包含region。在定義Layout時,我們要給出 template ,以及這個template中所包含的region。在渲染完layout之後,我們可以用所定義的regions顯示其他views。也就是我們要通過Layout的定義確定要在每個房間裡放上什麼,以及應該具備什麼功能。

在下面的TodoMVC Layout模組中,我們要定義下面兩個Layout:

  • Header: 我們可以在這裡建立新的Todo
  • Footer: 我們可以在這裡顯示還有多少Todo,以及做完了多少之類的彙總資訊

這樣,之前放在 AppViewTodoView中的一些view邏輯就被挪到這裡來了。

注意一下 ,Marionette module (比如下面這個) 實現了一個簡單的模組系統,用來在Marionette apps中實現私有化和封裝。然而我們不是必須要這麼做,還可以用 RequireJS + AMD 實現。

TodoMVC.Layout.js:

TodoMVC.module('Layout', function(Layout, App, Backbone, Marionette, $, _){

  // Layout Header View
  // ------------------

  Layout.Header = Marionette.ItemView.extend({
    template : '#template-header',

    // UI bindings create cached attributes that
    // point to jQuery selected objects
    ui : {
      input : '#new-todo'
    },

    events : {
      'keypress #new-todo':   'onInputKeypress'
    },

    onInputKeypress : function(evt) {
      var ENTER_KEY = 13;
      var todoText = this.ui.input.val().trim();

      if ( evt.which === ENTER_KEY && todoText ) {
        this.collection.create({
          title : todoText
        });
        this.ui.input.val('');
      }
    }
  });

  // Layout Footer View
  // ------------------

  Layout.Footer = Marionette.Layout.extend({
    template : '#template-footer',

    // UI bindings create cached attributes that
    // point to jQuery selected objects
    ui : {
      count   : '#todo-count strong',
      filters : '#filters a'
    },

    events : {
      'click #clear-completed' : 'onClearClick'
    },

    initialize : function() {
      this.bindTo(App.vent, 'todoList:filter', this.updateFilterSelection, this);
      this.bindTo(this.collection, 'all', this.updateCount, this);
    },

    onRender : function() {
      this.updateCount();
    },

    updateCount : function() {
      var count = this.collection.getActive().length;
      this.ui.count.html(count);

      if (count === 0) {
        this.$el.parent().hide();
      } else {
        this.$el.parent().show();
      }
    },

    updateFilterSelection : function(filter) {
      this.ui.filters
        .removeClass('selected')
        .filter('[href="#' + filter + '"]')
        .addClass('selected');
    },

    onClearClick : function() {
      var completed = this.collection.getCompleted();
      completed.forEach(function destroy(todo) { 
        todo.destroy(); 
      });
    }
  });

});

接下來我們要處理應用的路由和工作流,比如對頁面中Layout顯示還是隱藏起來的控制。

路由器 AppRouter 及 Controller

為了簡化路由,Marionette 引入了 AppRouter 的概念。用上它之後,就不用再寫那些路由事件處理的繁瑣程式碼了,並且可以將路由器配置為直接呼叫某個物件上的方法。我們用appRoutes配置AppRouter

原來用Backbone時,我們要在路由器Workspace中定義 '*filter': 'setFilter' 路由:

    var Workspace = Backbone.Router.extend({
            routes:{
                    '*filter': 'setFilter'
            },

            setFilter: function( param ) {
                    // Set the current filter to be used
                    window.app.TodoFilter = param.trim() || '';

                    // Trigger a collection reset/addAll
                    window.app.Todos.trigger('reset');
            }
    });

儘管我們用上了可讀性很強的Layouts,但原來在AppView and TodoView中的顯示邏輯並沒有挪過去,也要由TodoList 控制器處理。

TodoMVC.TodoList.js:

TodoMVC.module('TodoList', function(TodoList, App, Backbone, Marionette, $, _){

  // TodoList Router
  // ---------------
  //
  // Handle routes to show the active vs complete todo items

  TodoList.Router = Marionette.AppRouter.extend({
    appRoutes : {
      '*filter': 'filterItems'
    }
  });

  // TodoList Controller (Mediator)
  // ------------------------------
  //
  // Control the workflow and logic that exists at the application
  // level, above the implementation detail of views and models

  TodoList.Controller = function(){
    this.todoList = new App.Todos.TodoList();
  };

  _.extend(TodoList.Controller.prototype, {

    // Start the app by showing the appropriate views
    // and fetching the list of todo items, if there are any
    start: function(){
      this.showHeader(this.todoList);
      this.showFooter(this.todoList);
      this.showTodoList(this.todoList);

      this.todoList.fetch();
    },

    showHeader: function(todoList){
      var header = new App.Layout.Header({
        collection: todoList
      });
      App.header.show(header);
    },

    showFooter: function(todoList){
      var footer = new App.Layout.Footer({
        collection: todoList
      });
      App.footer.show(footer);
    },

    showTodoList: function(todoList){
      App.main.show(new TodoList.Views.ListView({
        collection : todoList
      }));
    },

    // Set the filter to show complete or all items
    filterItems: function(filter){
      App.vent.trigger('todoList:filter', filter.trim() || '');
    }
  });

  // TodoList Initializer
  // --------------------
  //
  // Get the TodoList up and running by initializing the mediator
  // when the the application is started, pulling in all of the
  // existing Todo items and displaying them.

  TodoList.addInitializer(function(){

    var controller = new TodoList.Controller();
    new TodoList.Router({
      controller: controller
    });

    controller.start();

  });

});

在這個應用中,Controller並沒有給總體工作流新增太多東西。通常來說,按Marionette的觀點,應用中的routers應該是深思熟慮後的成果。可是,開發人員經常濫用Backbone的路由系統,整個程式的工作流和邏輯都放到一個控制器裡。

結果每種可能的程式碼組合都不可避免地放到了router方法中,建立view,載入model,協調應用中的不同部分等等。Drick覺得這破壞了單一職責原則 (SRP) 和 關注點分離原則。

Backbone的router和history是為了解決瀏覽器特定問題的 - 管理 forward 和 back 按鈕。Marionette 認為應該把他們的權力關在籠子裡,不讓他們碰通過導航執行的程式碼。這樣應用有沒有router都能用。我們可以從按鈕點選事件上,從應用的事件處理器上,或者從router上呼叫controller的show方法,並且不管如何呼叫哪個方法,我們最終得到的應用狀態都是一樣的。

Derick 把他對這一主題的想法都寫在了他的blog上,你如果感興趣可以去看看:

model

最後,我們要定義表示Todo item的model和collection。

Todos.js:

TodoMVC.module('Todos', function(Todos, App, Backbone, Marionette, $, _){

  // Todo Model
  // ----------

  Todos.Todo = Backbone.Model.extend({
    localStorage: new Backbone.LocalStorage('todos-backbone'),

    defaults: {
      title     : '',
      completed : false,
      created   : 0
    },

    initialize : function() {
      if (this.isNew()) this.set('created', Date.now());
    },

    toggle  : function() {
      return this.set('completed', !this.isCompleted());
    },

    isCompleted: function() { 
      return this.get('completed'); 
    }
  });

  // Todo Collection
  // ---------------

  Todos.TodoList = Backbone.Collection.extend({
    model: Todos.Todo,

    localStorage: new Backbone.LocalStorage('todos-backbone'),

    getCompleted: function() {
      return this.filter(this._isCompleted);
    },

    getActive: function() {
      return this.reject(this._isCompleted);
    },

    comparator: function( todo ) {
      return todo.get('created');
    },

    _isCompleted: function(todo){
      return todo.isCompleted();
    }
  });

});

一切就緒,走你!

最後,我們要在index檔案中呼叫application物件中的start,把一切調動起來:

走你:

  $(function(){
    // Start the TodoMVC app (defined in js/TodoMVC.js)
    TodoMVC.start();
  });

打完收工!

出於Backbone而勝於Backbone的Marionette

一個真正優秀的框架,能不露聲色地從碼農手中接過那些重複的工作,而當碼農偶爾想發揮主觀能動性,做點不落俗套的事情時,它還不會讓你處處碰釘子,仍能一如既往地支援你。

有人說,Backbone不是真正的MVC框架,雖然它可能覺得委屈,但見到Marionette之後,它應該會心甘情願的接受這種說法吧。

現在有這樣一個任務,我們要在頁面中顯示一個人的資訊,模板如下所示:

<script type="text/html" id="my-view-template">
  <div class="row">
    <label>First Name:</label>
    <span><%= firstName %></span>
  </div>
  <div class="row">
    <label>Last Name:</label>
    <span><%= lastName %></span>
  </div>
  <div class="row">
    <label>Email:</label>
    <span><%= email %></span>
  </div>
</script>

接下來,我們會讓Marionette用這個機會來證明自己的藍。

套路化的渲染程式碼

看下下面這段程式碼,是用Backbone 和 Underscore 模板渲染檢視的典型實現。首先要有個模板,可以直接放在DOM裡,然後要用Javascript定義使用這個模板的檢視,並從model裡得到資料放到模板裡。是的,這也是MVC。

佛說:一沙一世界,一花一菩提,須彌芥子,皆存玄虛。大MVC裡套著無數個小MVC。

定義模板

<script type="text/html" id="my-view-template">
  <div class="row">
    <label>First Name:</label>
    <span><%= firstName %></span>
  </div>
  <div class="row">
    <label>Last Name:</label>
    <span><%= lastName %></span>
  </div>
  <div class="row">
    <label>Email:</label>
    <span><%= email %></span>
  </div>
</script>

定義view

var MyView = Backbone.View.extend({
  template: $('#my-view-template').html(),

  render: function(){

    // compile the Underscore.js template
    var compiledTemplate = _.template(this.template);

    // render the template with the model data
    var data = this.model.toJSON();
    var html = compiledTemplate(data);

    // populate the view with the rendered html
    this.$el.html(html);
  }
});

這些做好之後,可以建立view的例項,將model傳給它。然後把view的el加到DOM中,顯示的工作就完成了。

給model套上模板

var myModel = new MyModel({
  firstName: 'Derick',
  lastName: 'Bailey',
  email: 'derickbailey@gmail.com'
});

var myView = new MyView({
  model: myModel
})

myView.render();

$('#content').html(myView.el)

這是用Backbone定義,構建,渲染和顯示的標準實現。也就是我們所說的“套路化程式碼”,一遍遍的重複,每個專案,每個相同的功能性實現中都有這些程式碼。繁瑣,重複,毫無趣味。

接下來我們要請出 Marionette ItemView - 用它,view 定義可以變得更簡潔!

Marionette用ItemView拯救你

Marionette的所有view型別,除了 Marionette.View ,都自帶一個render 方法,可以幫你處理渲染的核心邏輯。放棄Backbone.View吧,給MyView換個型別,就可以用上這個方法。不用再自己給view實現render方法,渲染的工作就交給Marionette吧。我們還用Underscore.js模板和渲染機制,但可以不用關心具體實現了。所以,也不用寫那麼多程式碼了。

擴充套件ItemView

var MyView = Marionette.ItemView.extend({
  template: '#my-view-template'
});

程式碼就是這些,跟前面那個view實現的功能一模一樣。只要把 Backbone.View.extend 換成 Marionette.ItemView.extend, 然後刪掉 render 方法就行了。你還是可以用model建立view例項,在view例項上呼叫render方法,並以同樣的方式在DOM中顯示view。但view定義只剩一行了,只需要配置下模板就行。

勝在區域管理

在view建立好之後,一般要把它放到DOM中,這樣才能把它顯示出來。Backbone一般是用jQuery選擇器,並設定結果物件的 html()

顯示view

var myModel = new MyModel({
  firstName: 'Jeremy',
  lastName: 'Ashkenas',
  email: 'jeremy@gmail.com'
});

var myView = new MyView({
  model: myModel
})

myView.render();

// show the view in the DOM
$('#content').html(myView.el)

這還是那種比較繁瑣的程式碼。我們不應該手工呼叫 render ,還手工選擇顯示view的DOM元素。另外,這些程式碼也沒關閉之前附到DOM元素上的view例項。一大波殭屍正在朝你走來!

為了解決這個問題,Marionette 做了個Region物件 - 由它負責管理每個view的生命週期,以及在特定DOM元素中的顯示。

Region物件

// create a region instance, telling it which DOM element to manage
var myRegion = new Marionette.Region({
  el: '#content'
});

// show a view in the region
var view1 = new MyView({ /* ... */ });
myRegion.show(view1);

// somewhere else in the code,
// show a different view
var view2 = new MyView({ /* ... */ });
myRegion.show(view2);

上面的程式碼中有幾個事兒需要注意。首先,我們要告訴region要管理哪個DOM元素,在region例項中指定el。其次,我們不用親自呼叫view中的render方法。最後,我們也不用呼叫view中的close方法,Marionette替我們呼叫它了。

當我們用region管理view的生命週期,並在DOM中顯示view時,region自己就會把這些問題處理掉。把view例項傳給region的 show 方法,它會幫我們呼叫view上的render方法。然後會拿到view上的結果 el ,組裝DOM元素。

我們再次呼叫 show 方法時,region知道它現在正在顯示view。region會呼叫view上的 close 方法,把它從DOM上挪走,然後就執行新view上的 render & display程式碼。

既然region幫我們呼叫close,我們還在view例項上用bindTo 事件繫結器,就不用再擔心程式中出現殭屍view了。

勝在記憶體管理

除了縮減了view定義的程式碼,Marionette所有view中還有些先進的記憶體管理功能,使得view例項的清除工作和事件處理更容易了。

看下面的view實現:

var ZombieView = Backbone.View.extend({
  template: '#my-view-template',
  initialize: function(){
    // bind the model change to re-render this view
    this.model.on('change', this.render, this);
  },

  render: function(){
    // This alert is going to demonstrate a problem
    alert('We`re rendering the view');
  }
});

如果我們用相同的變數名稱建立這個view的兩個例項,然後修改model中的一個值,能看到幾次警告框?

建立兩個view例項

var myModel = new MyModel({
  firstName: 'Jeremy',
  lastName: 'Ashkenas',
  email: 'jeremy@example.com'
});

// create the first view instance
var zombieView = new ZombieView({
  model: myModel
})

// create a second view instance, re-using
// the same variable name to store it
zombieView = new ZombieView({
  model: myModel
})

myModel.set('email', 'jeremy@gmail.com');

按常理,因為兩個例項都用zombieView 變數名,所以view的第二個例項建立後,第一個例項立馬就會落到作用域之外。Javascript可以通過垃圾收集把它清除掉,也就是說第一個view例項不再有用,也不會對模型的“change”事件作出響應。

但執行這段程式碼時,警告框還是出現了兩次!

出現這種問題,是因為model事件繫結是在view的initialize 方法中。無論什麼時候把this.render當做回撥方法傳給model的 on 事件繫結,model也會得到一個對view例項的直接引用。就是因為model中有對view例項的引用,所以即使換掉 zombieView變數引用的view例項,不再被 zombieView引用的view例項也不會落到作用域之外。前面說過了,model中還有對它的直接引用。

既然最初那個view還在作用域內,第二個view例項也在,兩個例項自然都會對model中的資料修改做出響應。

解決這個問題很簡單。只要在用完view後呼叫model上的off 方法,讓這個事件繫結處於準關閉的狀態就行。為此,要在view里加個close 方法。

增加close方法的View定義

var ZombieView = Backbone.View.extend({
  template: '#my-view-template',

  initialize: function(){
    // bind the model change to re-render this view
    this.model.on('change', this.render, this);
  },

  close: function(){
    this.model.off('change', this.render, this);
  },

  render: function(){
    // This alert is going to demonstrate a problem
    alert('We`re rendering the view');
  }
});

不再需要第一個例項後,只需在其上呼叫close方法就行,活著的view例項就只剩一個了。

close方法演示

var myModel = new MyModel({
  firstName: 'Jeremy',
  lastName: 'Ashkenas',
  email: 'jeremy@example.com'
});

// create the first view instance
var zombieView = new ZombieView({
  model: myModel
})
zombieView.close(); // double-tap the zombie

// create a second view instance, re-using
// the same variable name to store it
zombieView = new ZombieView({
  model: myModel
})

myModel.set('email', 'jeremy@gmail.com');

再執行程式碼,就只剩一個警告框了。

有了Marionette,我們就不用自己寫程式碼關閉事件處理器了。

Marionette的記憶體管理

var ZombieView = Marionette.ItemView.extend({
  template: '#my-view-template',

  initialize: function(){
    // bind the model change to re-render this view
    this.bindTo(this.model, 'change', this.render, this);
  },

  render: function(){
    // This alert is going to demonstrate a problem
    alert('We`re rendering the view');
  }
});

注意上面的程式碼,我們用bindTo方法取代了on方法。這個方法在Marionette的EventBinder物件中定義,在所有view型別中都能用。 bindTo 的方法簽名跟on方法差不多,只是把觸發事件的物件不再是呼叫者,而是變成了方法的第一個引數。

Marionette的view中還有一個 close方法,用來'off'掉事件的繫結。用bindTo設定的事件繫結會被自動關閉。也就是說我們不用親自定義,和呼叫 close 方法,只要用bindTo 方法設定事件繫結,我們就可以放寬心,事件繫結會被自動'off'掉,我們的views也不會變成殭屍。

但view上 close 方法的自動呼叫是怎麼實現的呢?什麼時候,以及在哪呼叫它呢?請看Marionette.Region - 這是負責管理每個view生命週期的物件。

相關文章