Marionette View的生命事件模型

本文略译自《The Life of a Marionette View

有过前端经验的人都了解,在构建单页面应用程序时,页面常常有一些UI组件需要被动态替换,要确保它们被正确创建、使用和销毁,不然产生内存问题。所以,管理页面上的「UI组件的置换」是很重要的任务,Marionette 在View基类中增加了“生命周期”的事件“勾子”,达到了简化管理「UI组件的置换」的任务。

什么是生命周期事件?什么是事件勾子?

生命事件勾子的原由

构建交互功能丰富的用户界面是不容易的。挑战来源于「UI构建」交叉了多个知识领域,包括 Software Development, Graphic Design, User Experience and Information Architecture。由于「用户界面的代码」要控制复杂的界面呈现(例如V嵌套)、动态交互逻辑(交互功能复杂)和直接面对用户的操作,所以动态UI代码的实现不容易分析。

还好,软件工业演化了30多年,我们有很多成熟的技术可以依靠。例如,我们可以使用软件工程的构件化技术分解和设计复杂UI功能,具体的说,我们可以将复杂的用户界面分解一个个独立UI组件(解决了结构的复合性),另外,这些组件都有独立的“life cycle”——组件的创建和销毁过程,我们可以在组件的创建和销毁过程中注入“事件”(解决组件动态性),实现更多的动态交互功能。

生命事件勾子的意义

View(UI)组件有一个清晰的生命周期,在帮助开发者「构建复杂交互界面功能」上有多种益处:

  • 第一,简化对「V组件在使用过程中的状态变化」的分析;
  • 第二,简化制作「两个V组件之间的通信协作」的功能;
  • 第三,创建约定的事件接口,开发者能添加自定义行为来定制V组件的额外功能,这不涉及V组件的内部实现,可读性好,也降低了boilerplate code;
  • 最后,平滑集成V组件的外部代码,这些代码会根据V组件的不同状态来运行(EM:这个与第二,第三有什么区别?)

Marionette Views 在 Backbone Views 之上增加的 life cycle API,满足了以上的开发需求。

EM:Mn的 V组件,是一种「抽象高级的」程序对象的生命,这种对象是抽象过的应用(业务)对象,比较的复杂,例如能嵌套子V;Mn本身已经在创建「V生命」中完成了很多工作,生命事件勾子,只是用于定制。

一种事件代表生命中重要一步

The Marionette View实例是一个“存活”在内存对象,它有一个被创建、使用和销毁过程,就像一支生命物; life cycle是指这个生命过程,生命事件是人为在这个过程标识出某种重要步骤的发生。Marionette在制作View基类时在相应的生命步骤设计了事件勾子——一个可被重载的虚函数,当基类被派生时可以自定义事件勾子。

生命事件与及勾子(处理函数)的命名约定

Marionette的生命事件与及勾子有一个命名的约定:

  • 第一,在生命步骤eventname之前发生的事件 约定名为 before: ,事件勾子叫 onBeforeEventName;
  • 第二,在生命步骤eventname之后发生的事件 约定名为 eventname ,事件勾子叫 onEventName;

例如,要为 ExampleView 渲染事件前/后,对ExampleView进行定制,它的事件勾子的定义如下:

var ExampleView = Mn.ItemView.extend({

onRender: function() {
    // do stuff after the View is rendered
},

onBeforeRender: function() {
    // do stuff before the View is rendered
}

});

渲染(Render)只是 View类比较重要的「生命事件」之一,接下来我会介绍 Marionette的View类对象及其它相关对象(application, region等)的生命事件,并简要解释它的常用用例,文档在这里

纯一View生命事件

所谓纯一View是指没有嵌套内层子View的View。

对象创建(Initialize)

View生命周期中的第一个事件是 initializeinitialize 不是 Marionette 特有的,它继承自Backbone,虽然如此,确保你了解它在Marionette 的 View 的生命周期中的角色和位置,还是很重要的。

Backbone提供了在实例化 View 时运行的两个函数:constructorinitializeconstructor,从名字可推知,是用于 Backbone 和 Marionette 的类对象内部初始化构造,一般不属于事件勾子,不用于定制,通常不应该被重载。

initialize则属于对象实例的构造函数,可以重载来添加自定义构造函数,定制对象实例。重载initialize一般设定与M的监听关系(setting up event listeners),并且可以修改构造参数options,进行对象实例的定制。

View初始化事件(initialize) 是特殊View生命周期中的事件,证据是在它只有一个“事件勾子”。

EM:更主要的是,initialize 是生命的起点,它没有其它的需要的必要条件要提前准备。相对的,后续生命事件,在事件前、事件和事件后,都有相应「条件准备」的需求——前一件事为后一件事准备条件。例如onRender想修改一个View的el内容,它依赖Render渲染出View的el。

UI结构渲染(onRender/onBeforeRender)

View 生命周期中的第二个重要步骤,是UI结构渲染(render)。渲染是View作为一种计算对象的核心计算任务,渲染的具体涵义在技术上是指「el这个属性的制作」,制作的数据来源的可以是HTML字串,可以是template编译等。官方说,只有使用template制作el时才会触发事件勾子。使用预渲染内容则会被忽略。

理解渲染的确切涵义,可以与「第三步挂接DOM」进行比较。我们可认为渲染一步是为挂接上DOM树做数据准备。

渲染前( before:render)和渲染后( render )事件勾子适用于定制一些el额外处理,例如使用第三方模板引擎,和处理子View

渲染前( before:render)适用于移除子 View,而渲染后( render )适用于追加子View的渲染内容(见例子),因为第一次Render 通常发生在View挂接上DOM之前。

import MyChildView from './MyChildView';

const MyView = View.extend({ template: _.template('

'), regions: { 'foo': '.foo-region' }, onRender() { this.showChildView('foo', new MyChildView()); } });

`为什么增加生命事件和勾子 Marionette的View类已经默认「自动实现了很多的生命任务」,你只需通过一种简单的声明式定义,即可创建View对象; 那为什么还要设立生命事件勾子 呢?难道说,View的对象(的形式部分)还有一些不用能用声明,用UI模板来完成形式定义?

`声明定义View的形式 View的形式构建分两个部分: 第一,呈现的界面内容;第二,界面的UI交互事件安装; 界面内容有template,事件有event hash 留给生命事件勾子什么事? 答案一,反正就是定制。一个典型的例子 before:Render事件可以定制使用模板引擎,避开Marionette提供的

`声明定义之外的制定 原义上,事件勾子都做一些额外定制的事,View类本身自动完成生命任务主体(生命活动,包括构造、活动和销毁),例如你可定制Render/Attach,View类本身具有这些功能,你不定制它也是functional的

DOM挂接(onAttach/onBeforeAttach )

当View的结构形式(包括子View)通过渲染准备好后,下一步就是挂接上DOM树。将View的生命过程的渲染和挂接分开,是因为有一个些View的使用是需要预先渲染(而不必挂接上DOM),渲染EL独立是便于中间处理,另外挂接DOM会重绘页面,很耗损性能,尽量减少挂接的次数。

挂接上DOM树后,用户可看见交互UI界面,进行交互。所以,挂接事件勾子(onAttach)可以添加DOM-dependent的处理,例如jQuery插件的安装。文档表明,事件勾子(onAttach)是最理想的时机,为 「view的 el本身」而不el的内容安装jQuery插件,要为「el内容」安装jQuery插件,最好使用onDOMRefresh。

EM:挂接还有一个相对的相反的卸接(detach)一步。

对象销毁onDestroy/onBeforeDestroy

View生命最后一步是 destroy ,它算是 Initialize 的反向,适用于添加自定义的清理(这里被清理的东西一般是Initialize事件安装的非built-in功能)。此生命步骤还有一项设计目标,就是Marionette团队希望提供一种让开发者确定销毁对象的最后机会,开发者可以在生命勾子 onBeforeDestroy 中防止对象被误删。

复合View动态生命事件

以上四步是单一交互UI的生命步骤,而对于复杂的View,则还要了解处理嵌套子View,和动态重渲染的生命事件。

显示出子View(onShow/onBeforeShow)

Show 生命步骤是「管理View特定的」,是View manager的生命事件;由于View可以是 View manager的一种,所以Show也算是View生命步骤,但是官方没有规定View应该何时触发show subview,所以 show 事件是任意时刻的。不过,官方建议在onRender勾子上show subview。

init, render,attach,show意思含混

init, render,attach,show其实都是在创建一个View生命实例,区别在哪里?我怀疑init和show有重复,view都是被一个mgr管理的,init和show都是触发生命的起点。

show和init的意义其实是相似,都是要在一个View容器上创建/初始化一个view对象。而我们要理解在onShow勾子上做什么,得先知道MN已经在Show上自动为我们完成了什么,而我们最关心的点是,region如何实现交互功能模块化(UI功能逻辑分割),并且保证效率(任意渲染一次挂接),从文档可了解到,最新版MN已经为我们完成了大部分工作。包括Show时自动渲染子V,保证一次挂接,和高效递归嵌套子V

当我通过声明定义来创建内嵌子V时,MN不会自动帮我处理么?从以下文档片断可见,MN只自动部分子V创建,必须手动onRender勾子上显示子V:

Showing a Child View

To show a view inside a region, simply call showChildView(regionName, view). This will handle rendering the view's HTML and attaching it to the DOM for you:

import _ from 'underscore'; import { View } from 'backbone.marionette'; import SubView from './subview';

const MyView = View.extend({ template: _.template('

Title

'),

regions: { firstRegion: '#first-region' },

onRender() { this.showChildView('firstRegion', new SubView()); } });

onDomRefresh

Dom Refresh的生命对象不是DOM,因为DOM只是 View 形式的一部分。 dom:refresh 应该也是View 生命的一步,只是比较特殊。它是指View(完成实例创建后)在使用过程被动态改变形式后(el在DOM上的内容),触的重渲染重创建的事件。对一些高级交互功能,UI内容被动态改变是常见的。

CollectionView生命事件

onBeforeAddChild/onBeforeRemoveChild/onAddChild/onRemoveChild (CollectionView Only)

Applications生命事件

Marionette Applications 是Marionette用来初始化应用程序的对象,它也有一组生命周期事件,onStart and onBeforeStart 具体从略。

机器中的齿轮

Marionette’s View生命事件模型最大的优点,是它提出一种构建对象系统的标准模式。这也是Marionette成功的基础,相对于其它应用框架莫名类型设计(type of magic),Marionette的设计更易理解和使用。

挑选你想要的

Marionette不强迫我们在创建View中使用它设计的生命事件模型,但是由于它用了一个相对简单的概念模型(simple mental model )对View的生命过程进行表达,我们没有理由不使用,而需要自己另外构建。

参考

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.