ExtJS 核心概念 - 组件的生命周期

ExtJS 的组件生命周期分为三个阶段:

  • 初始化
  • 渲染
  • 销毁

准确理解组件的生命周期过程,是正确使用 ExtJS 框架的基础。尤其当你需要编写 ExtJS 的插件或对其做其它扩展时,更应该知道组件详细的生命周期过程。

ExtJS的组件生命周期

从图中可以看出,ExtJS 的组件始于初始化终于销毁,而且组件未必需要经过渲染即可进入销毁阶段。不同版本的 ExtJS 处理的细节必然不尽相同,我很难告诉你一个永远正确的、详实的生命周期过程,因此我以 4.2 版本为蓝本讨论其生命周期中具体的处理过程,希望给你一个方向性的指引,而你应该根据手中具体的版本结合API文档,甚至是源代码来确定生命周期过程中的每一个细节。值得庆幸的是,我在 ExtJS 3.4/4.2/5.1 三个版本中,都没有发现其生命周期有很大的变动。

另一件值得补充的事是,我在实践中发现有不少人知道 “ExtJS 存在生命周期” 这么一回儿事,然而并不清楚这和自己的代码有什么关系。我想在正式讲解前告诉你,当你 Ext.create (或 new) 一个组件时,它的生命周期过程就会开始,例如:

Ext.create('Ext.grid.Panel', {
    title: 'MyPanel',
    height: 200,
    width: 400,
    store: ...,
    columns: ...
});
1
2
3
4
5
6
7

我们创建了一个 ExtJS 的列表组件,此时它会完成生命周期中的 “初始化” 阶段,当我们在浏览器中执行以上代码时并没有看到列表页面,因为组件还没有渲染。如果我们配置了 renderTo 属性,则它会在初始化后直接进入渲染阶段。

你在无意中操作生命周期的情况也有很多,又如,通过调用 add 方法给 toolbar 组件增加新的按钮:

toolbar.add([
    {text: 'Button 1'},
    {text: 'Button 2'}
]);
1
2
3
4

这一过程通常也会经过“初始化”和“渲染”阶段。我们只是传入了一个极其普通的 JavaScript 对象,ExtJS 框架内部会帮我们创建、初始化 Button 对象并将其渲染到相应容器(此例为 toolbar)中。如果你想自己完成初始化操作,可以这样:

var anothorBtn = Ext.create('Ext.button.Button', {
    text: 'MyButton'
});
toobar.add(anothorBtn);
1
2
3
4

两种方式稍有区别,后者可能会过早地创建一个也许永远用不着的组件。现在来看看具体的生命周期过程:

初始化

初始化是组件开始构建的起点,你可以粗略地认为,本阶段仅是创建 JavaScript 对象的过程,也正因此初始化可能是执行速度最快的。这一阶段会进行必要的配置信息处理、注册一些基础事件,甚至会做预渲染工作。

1. 配置应用

优先处理参数中传入的配置信息。通常这一过程只是把配置对象复制到想要创建的 ExtJS 组件中以备后续使用。initialConfig 中保存着你传入的原始参数。代码实现看起来是这样的:

if (config) {
    Ext.apply(this, config);
} else {
    config = {};
}
this.initialConfig = config;
1
2
3
4
5
6

2. 注册事件

之后会注册监听事件,注意是 注册 而非触发或绑定事件。事件注册过程就像变量声明,只是声称当前组件有哪些事件,此时应该在文档中描述清楚这些事件何时会触发。ExtJS 提供了注册事件的方法,非常方便:

this.addEvents('activate');
1

3. 生成ID

如果你通过参数传入了 id 则使用传入的,否则将会生成新的。ExtJS 的代码实现非常有趣,我在 ExtJS 2.2 版本中初次见到这个方法时,就觉得它是个不错的 JS 技巧,直到现在ExtJS 还在使用它,来欣赏一下:

getId: function () {
    return this.id || (this.id = 'ext-comp-' + (this.getAutoId()));
}
1
2
3

插句题外话。我建议应用中除了公共的、使用频率极高的组件,尽量不要明确指定 id ,毕竟 id 最终会被用来生成 html 标签的 id 属性。此时重复的 id 将导致不可预知的后果。如果你希望在某组件中快速地找到子组件,使用 itemId

4. 实例化插件

插件的实例化过程是早于 initComponet 的,以便于像 gridpanel 的 editing 这类插件提前处理 editor 等。

5. initComponent

使用过 ExtJS 的人都知道这个方法。initComponent 方法的设计动机是给子类一个切入点,使其可以友好地参与父类的初始化过程。在方法中你可以:注册子类特有的事件,创建对 store 的引用,创建并实例化子组件等。通常你只需要覆盖并实现此方法来扩展某组件的初始化过程即可。

6. 组件注册到 ComponentManager 中

此步会将组件注册到 ComponentManager 中,它是 ExtJS 的组件管理器,当你调用

Ext.getCmp('foo');
1

时,实际上正是从组件管理器中找到对应组件。关于 ComponentManager 详细的介绍可以阅读API文档,唯一需要提醒你注意的是,不要使用重复 id ! 后注册的相同 id 的组件会覆盖之前的。

7. 增加 observable 和 state

observable 为组件提供事件注册、触发及监听绑定的能力;state 为组件提供状态信息,如用户拖动的表格列宽、当前打开的标签页等。ExtJS 在生命周期过程中通过 mixin 的方式掺和了这两个能力,换言之,如果我们扩展的组件继承自 ExtJS 的任意一个组件,我们不需要显式地重复调用此二者,因为 ExtJS 已经为我们调用了:

this.mixins.observable.constructor.call(this);
this.mixins.state.constructor.call(this, config);
1
2

8. 注册状态事件 resize

在上一步骤中注入了 state 能力,然后为组件绑定 resize 事件,以便组件随容器自动改变大小。这一步骤是 ExtJS 所有组件能够完成屏幕自适应的基础。

9. 初始化插件

也就是调用插件的 init 方法。在之前的步骤中已经实例化了所需插件,此步骤调用并执行插件。

10. 处理 ComponentLoader

ComponentLoader 提供了一种可能:通过Ajax传回的内容构造组件。详细使用参考API文档。

11. 进入渲染阶段

如果指定了 renderTo 属性,则继续渲染组件,否则等待用户调用渲染方法,或通过父容器调用渲染方法。 如果是 “非容器内” 组件且配置了 autoshow 属性,则 show 组件。

渲染

组件成功初始化后就具备了渲染条件,渲染是组件实际绘制到浏览器上并展示给用户的过程。此阶段通常最慢,如果组件极复杂,渲染过程会消耗大量的CPU。我们前面提到过,如果你在创建组件时就指定了 renderTo 属性,组件会在初始化后直接开始渲染,当然你也可以通过调用 render 方法手动开启渲染过程:

myCmp.render('myDivId');
1

render 方法会将你的组件渲染到指定的 DOM 元素上。也许你会有疑问:“我的整个 ExtJS 项目中,根本没有出现过 renderTo 或 render 字样,可是页面不依然正常渲染了吗?”。很好,这至少说明你正在使用并认真观察过 ExtJS 的样子。你需要知道的更多:首先,ExtJS 的某些特定组件并不需要指定 renderTo 也很清楚自己往哪里渲染。例如 viewport 组件,它总是会渲染到 document body 上;另外,如果你把某组件放到另一个父容器组件中,父容器组件内部会根据当前布局情况自动调用 render 方法。

知道了这些,让我们来看看渲染阶段的具体步骤:

1. 触发 beforerender

首先触发 beforerender 事件。如果任一绑定到组件的 beforerender 事件函数中返回了 false,都将会阻止渲染过程的继续进行。如果你想在组件 初始化 之后 渲染之前做一些特定处理,绑定 beforerender 是个不错的选择。

必须在组件渲染之前就绑定 beforerender 事件,渲染之后绑定的该事件无效(除非重新渲染或重复渲染)。这个道理很容易懂,不需要我多解释。然而很奇怪的是,我在实际项目中屡屡发现有些朋友犯这类错误。我想主要原因可能是当组件众多且关系稍有复杂时,他们便搞不清各组件正处于生命周期中的哪个阶段了。此类问题我无法给你一个万能的解决方案,但有一点可以确信:搞清楚 ExtJS 的组件生命周期,不断地独立调试,此类问题会越来越少。

2. 缓存

之后会用 Ext.Element 包裹 DOM 元素,并做一些缓存工作。缓存过程中也会进行预渲染,如果你有兴趣可以去研究 ProtoElement 对象(注意它在 ExtJS 4.2 中是私有的),否则你只需要知道:ExtJS 为 DOM 元素做了缓存。

3. 处理浮动

设置了 floating 的组件会被 WindowManager 管理,它需要处理 z-index 及焦点问题。当使用 WindowMenu 时,建议多关注一下此步骤。

4. 设置容器

容器有两种:一种是普通的 div,例如你通过 renderTo 指定了 DOM 元素,ExtJS 会在其下创建一个 div 并用 Ext.dom.Element 包裹,再将它作为当前组件的容器;另一种是已有的组件,此时该组件会成为当前组件的父组件。

容器为 ExtJS 的组件提供了依存环境,通俗地讲,设置容器的过程解决了这样一个问题:构成当前组件低层的这么一大堆 HTML ,最终要被插入到页面上的哪个 HTML 下呢?

5. onRender

执行 onRender 方法。该方法之于渲染的重要性,如 initComponent 之于初始化。它是子类扩展渲染过程的切入点,通常子类中应该先调用父类的 onRender 方法,以便保证所有的核心 DOM 结构已经处理完毕。

6. 设置 hideMode 和 overCls

要了解 hideMode 你需要阅读API文档并学习相关 CSS 知识;overCls 用来为鼠标划过时增加额外的样式。

7. 触发 render

触发 render 事件。此时所有的组件使用的 HTML 元素已被注入到 DOM 中,所有的样式也正确应用并处理。在 render 事件中操作组件自身的 DOM 是安全的,不过你需要清楚地知道,此时并非组件在页面中展示的最终形态,接下来要讲的后续操作有可能改变组件的结构、样式等。

8. afterRender

afterRender 是又一个实用的模板方法。渲染过程中调用此方法的主要目的是,完成一些渲染的收尾工作。这一阶段可以处理组件的大小(width/height)、组件对齐或增加 HTML style 等。如果需要,还会处理 resize , 处理滚动问题,以及拖拽等。子类中同样不要忘记调用父类的 afterRender 方法。

9. 触发 afterrender

afterrender 事件被触发时,你可以放心地操作 DOM 了。此时组件已经接近最终形态,除了是否显示。

10. 设置 hidden

如果组件被设定为 hidden ,将其对应的 HTML 元素隐藏起来。

销毁

死亡对于我们来说是人生中的一件大事,对于组件亦然,销毁过程自然是指为组件处理后事。这些后事包括:从 DOM 树中将其移除;从 ComponentManager 中将其注销;销毁所有绑定到组件的事件等。

通过调用 destroy 方法即可销毁组件。当然多数时候你并不需要手动写代码调用它,组件的父容器在销毁时会递归调用子组件的 destroy 方法。另外,像 window 等组件的 close 方法中,也会视配置情况自动调用 destroy 方法。

当你开发自己的组件时,合理地销毁组件是很重要的。例如,如果我们在组件渲染时绑定了自定义的事件,而此事件没有被及时销毁,当再次渲染组件时,事件会被重复绑定至底层的 DOM。有时,这类程序缺陷并非那么容易被发现,因为相同的代码反复执行多次并不一定会产生副作用,它只是在渐渐地拖慢你的应用而已。而当用户刷新浏览器时,世界又再次变得美好。

但如果不是简单的绑定事件呢?如果你那存在缺陷的代码会去反复创建另一个复杂的组件呢?如果你多次执行的是非幂等操作呢?所以请我们尊重每一个组件,让它们能够善始善终。

来了解一下销毁过程:

1. 触发 beforedestroy

和许多事件一样,如果任一绑定到组件的 beforedestroy 事件函数中返回了 false,都将会阻止销毁过程的继续进行。

2. beforeDestroy

调用 beforeDestroy 模板方法。它和前面介绍的模板方法类似,在销毁前调用。

3. 注销浮动

如果组件设置了 floating ,这一步骤会从 ZIndexManager 中将浮动组件注销。

4. 从父容器中删除

接下来调用父容器的 remove 方法,从父容器中删除组件:

this.ownerCt.remove(this, false);
1

5. onDestroy

onDestroy 是实际销毁组件的方法,它也是一个模板方法。在 ExtJS 的组件中,此步骤会解除 resize 事件的绑定、销毁与当前组件关联的浮动组件(如 loadMask),它还会删除绑定到组件上的延迟任务等。如果你自定义并扩展了 ExtJS 的组件,不要忘记在此步骤中回收一切应该回收的资源,让它们连同你的组件一起被销毁。

6. 销毁插件

销毁插件的过程即是依次调用各插件的 destroy 方法的过程。所以,当你编写插件时,不要忘记为其编写 destroy 方法。

7. 解除 HTML 事件绑定并销毁元素

如果组件已经被渲染,则将其对应的 HTML 元素中绑定的事件解除,并销毁元素。如果组件初始化后没有渲染(如 store,通常不需要渲染),则什么也不做。

需要指出的是,此步骤解除的是绑定到底层 HTML 元素上的事件,而非绑定到 ExtJS 组件上的事件。一般情况下,绑定到组件的事件会被框架内部继续绑定至 HTML 元素,你不需要手动为 HTML 绑定事件。此类事件(如 click)需要用户通过浏览器实际发生交互才会触发。还有一种事件是 ExtJS 组件特有的,例如 render 事件,它只在 ExtJS 组件生命周期过程中触发,不会被绑定到 HTML 元素上。经过 ExtJS 的精心封装,这两类事件在代码层面来看并没有什么区别,但我希望你还是能正确区分它们。

8. 触发 destroy

触发 destroy 事件。请注意这个事件触发的时机,在它之前我们已经销毁的 DOM 元素,因此,你不能在 destroy 事件中操作 DOM 树。

9. 从 ComponentManager 中注销组件

我们在初始化阶段,将组件注册到 ComponentManager 中,就像为每个组件办理身份证并备案.现在,我们理应为其销户,将组件从 ComponentManager 中注销。

有趣的是,ComponentManager 的管理方式和我们现实生活中很像。注销组件仅仅代表组件不受 ComponentManager 管理,你不再能通过 Ext.getCmp() 得到它,然而并不代表组件真正被销毁。就像你去注销身份证,并不代表本人已经死亡。当然对于组件,尤其是 ExtJS 提供的现有组件,从 ComponentManager 中注销前应该会被销毁。

10. 解除事件绑定

此步骤解除的是 ExtJS 组件的事件。

模板方法模式

如果你有兴趣研究得更深入些,模板方法模式 是你必须理解的设计模式。模板方法模式大致的思路是,定义一个操作中的代码骨架,将一些具体的步骤延迟到子类中实现,使得子类可以不改变代码结构即可改写或扩展某些特定步骤。它非常适合生命周期过程的控制。

这里给出 Java 实现模板方法模式的代码示例:

public abstract class Component {
    abstract void onRender();
    abstract void afterRender();
    
    public void render() {
        onRender();
        afterRender()
    }
}

class GridPanel extends Component {
    void onRender() {
        // ...
    }
    void afterRender() {
       // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ExtJS 使用此设计模式实现生命周期过程的控制,它提供了足够多的模板方法供子类扩展。例如我们之前讲到的 initComponentonRenderafterRenderonDestroy 等,它们均是在父类中提供骨干实现甚至空实现,然后在子类中不断地扩展。如果你打算研究 ExtJS 的源码,一定要先掌握该设计模式。

模板方法 OR 事件?

有一个问题,我想用过一段时间 ExtJS 并思考过的人一定疑惑过:当我们扩展一个组件时,究竟是覆盖实现自己的模板方法,还是为其绑定事件呢?例如我们想在组件销毁前做些自己的操作,是实现 beforeDestroy 方法还是绑定 beforedestroy 事件来处理呢?

ExtJS 提供了相当丰富的API,丰富到让人觉得混乱。从代码功能上讲,两者几乎是一样的,你可以随你的喜好来决定。而从设计上讲,两者确实有些区别。我不敢说“什么情况下用哪种方式一定最好”,毕竟同一项目中,一致的设计、一致的代码风格会更加重要,你应该优先遵从项目要求。ExtJS 官方也并没给出解释,我只是谈谈我的体会。

我觉得,当你继承某组件来自定义一个组件时,应该优先使用模板方法来扩展功能。在你的组件内部,尽可能地不要绑定事件。事件机制应该提供更大范围的解耦,而组件之间的继承关系,相对来说耦合度要高许多。当你定义好组件,并在其它场合使用它时,使用事件可能更好。通过在 controller 中绑定事件,将两个或多个组件之间的调用关系分离,各组件只关注自己的行为能力,只需要关心自己能做什么即可,而不用在意有哪些其它组件会调用它。如果用事件的方式代替模板方法,常常会造成组件自己声明事件,自己触发事件,又自己绑定事件的尴尬情况。这有点像你单身一人住,做好早饭时吆喝了一声:“早饭做好了!”,然后一个人默默地坐下来吃饭。省省吧,别吆喝了直接吃!

基于以上思路,我认为下面的代码并不理想:

Ext.define('MyPanel', {
    initComponent: function () {
        this.on('beforedestroy', this.myBeforeDestroy);
    },
    
    ...
    
    myBeforeDestroy: function () {}
});
1
2
3
4
5
6
7
8
9

现在你知道了 ExtJS 组件的全部生命周期过程,你也知道了如何用模板方法或绑定事件的方式扩展它,选一个你喜欢的组件,把它提供的全部模板方法、全部事件(ExtJS 的每个组件都有自己的新扩展,并不局限于本文章介绍的基础过程)都扩展一下,哪怕只 console.log 一下,来看看它是怎么执行的吧!


@ssbunny 2015-07-09

Last Updated:
Contributors: ssbunny