在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生命周期的对象。