2.6 事件编程的其他细节

至此,我们已经讨论了事件编程的核心及其在几种语言中的实现。在实际应用中,事件编程还有一些细节的议题。

2.6.1 收听者的执行顺序

收听者是否按照添加到发布者的顺序执行,通常是无关紧要的。发布者可以保持这个顺序,也可以采用其他顺序或者无序执行,这都取决于具体的实现代码。

2.6.2 收听者是否在单独的线程执行

发布者可以在自己所在的线程上逐个运行收听者,也可以为它们开设一个新的线程,甚至为每个收听者启动一个独立的线程。采用哪种方案也取决于实际环境和需要。在上面的Java和C#的例子中,Swing和Windows Forms图形用户界面框架默认都是在发布者所在的线程上运行所有收听者。在第3章会看到这个线程就是运行所谓主循环或者可称为事件分派循环的线程,它首先创建和显示应用程序的用户界面,然后不停地从操作系统读取该应用程序负责的事件,再将它们分派给各个控件内注册的收听者,并且逐个运行这些收听者。这种方式可能会产生一个问题,如果某个收听者的执行时间很长,例如需要等待网络返回的结果,那么在它运行结束前,用户在界面上的其他操作都会被阻塞而没有响应,例如单击一个按钮后,再选择视窗上的组合框和其他标签页这样简单的动作,界面都没有更新。解决办法就是减轻负责图形用户界面的线程的负担,为事件收听者新开一个线程,进行异步调用。例如,浏览器在一个标签页等待和载入网页时,可以继续执行用户命令,浏览其他标签页。Lotus Notes客户端在打开一个索引更新耗时较长的视图时,能够在后台更新的同时让用户先打开其他视图和文档。很多支持多线程编程的语言也都能够方便地进行函数的异步调用,例如,C#中的代理就自带BeginInvoke的方法。

2.6.3 控件层次中的事件传播

很多控件都可以作为容器容纳其他控件,被容纳的控件又可以容纳控件,如此组成的控件层次中,发生的事件由哪个控件处理也有多种选项。在Swing和Windows Forms中,事件仅仅由层次中最底端的控件处理。例如,视窗上有一个面板,面板里有一个按钮,给这三个控件的鼠标单击事件都添加收听者,在按钮上单击时,只有按钮的收听者会被运行。网页所用的DOM模型则复杂得多。事件会先从最顶层的元件传播到最底层,再反过来从最底层上升到最顶层。收听者在前一阶段接收事件称为捕获(Capturing),后一阶段称为冒泡(Bubbling)。元件在添加收听者时可以通过参数指定收听者工作于哪个阶段,默认是冒泡阶段。假设网页上有两个DIV元件,其中一个包容另一个,被包容的DIV里又有一个按钮,同样给这三个元件的鼠标单击事件都添加收听者,然后单击按钮。在冒泡模式下,按钮的收听者先运行,然后是其上一级的DIV元件的收听者,最后是最上层DIV的收听者。捕获模式下的顺序正好相反。需要时,收听者还能够调用事件参数的stopPropagation方法终止事件的传播,无论是在捕获还是冒泡阶段。