# 新时代新潮流WebOS 【22】WebKit，鼠标引发的故事

- 作者：邓侃
- 发布时间：2009-06-20 00:41
- 分类：公司
- 原文：https://www.ifanr.com/16902

![Figure 1. JavaScript onclick event ](https://s3.ifanr.com/wp-content/uploads/2010/08/114.jpg "Figure 1. JavaScript onclick event ")

*Figure 1. JavaScript onclick event*  
先看一段简单的HTML文件。在浏览器里打开这个文件，将看到两张照片。把鼠标移动到第一张照片，点击鼠标左键，将自动弹出一个窗口，上书“World”。 但是当鼠标移动到第二张照片，或者其它任何区域，点击鼠标，却没有反应。关闭“World”窗口，自动弹出第二个窗口，上书“Hello”。

> &lt;html&gt;  
> &lt;script type=”text/javascript”&gt;  
> function myfunction(v)  
> {  
> alert(v)  
> }  
> &lt;/script&gt;  
> &lt;body **onclick=”myfunction(‘Hello’)”**&gt;  
> &lt;p&gt;  
> &lt;img **onclick=”myfunction(‘World’)”** height=”250″ width=”290″ src=”http://www.dirjournal.com/info/wp-content/uploads/2009/02/antarctica\_mountain\_mirrored.jpg”&gt;  
> &lt;p&gt;  
> &lt;img height=”206″ width=”275″ src=”http://media-cdn.tripadvisor.com/media/photo-s/01/26/f4/eb/hua-shan-hua-mountain.jpg”&gt;  
> &lt;/body&gt;  
> &lt;/html&gt;

这段HTML文件没有什么特别之处，所有略知一点HTML的人，估计都会写。但是耳熟能详，未必等于深入了解。不妨反问自己几个问题，

1. 浏览器如何知道，是否鼠标的位置，在第一个照片的范围内？
2. 假如修改一下HTML文件，把第一张照片替换成另一张照片，前后两张照片的尺寸不同。在浏览器里打开修改后的文件，我们会发现，能够触发弹出窗口事件的区域面积，随着照片的改变而自动改变。浏览器内部，是通过什么样的机制，自动识别事件触发区域的？
3. Onclick 是HTML的元素属性(Element attribute)，还是JavaScript的事件侦听器(EventListener)？换而言之，当用户点击鼠标以后，负责处理onclick事 件的，是Webkit 还是JavaScript Engine？
4. Alert() 是HTML定义的方法，还是JavaScript提供的函数？谁负责生成那两个弹出的窗口，是Webkit还是JavaScript Engine？
5. 注意到有两个onclick=”myfunction(…)”，当用户在第一张照片里点击鼠标的时候，为什么是先后弹出，而不是同时弹出？
6. 除了PC上的浏览器以外，手机是否也可以完成同样的事件及其响应？假如手机上没有鼠标，但是有触摸屏，如何把onclick定义成用手指点击屏幕？
7. 为什么需要深入了解这些问题？ 除了满足好奇心以外，还有没有其它目的？

![Figure 2. Event callback stacks ](https://s3.ifanr.com/wp-content/uploads/2010/08/22.gif "Figure 2. Event callback stacks ")

*Figure 2. Event callback stacks*  
当用户点击鼠标，在OS语汇里，这叫发生了一次中断(interrupt)。系统内核(kernel) 如何侦听以及处理interrupt，不妨参阅“Programming Embedded Systems” 一书，Chapter 8. Interrupts。这里不展开介绍，有两个原因，1. 这些内容很庞杂，而且与本文主题不太相关。2. 从Webkit角度看，它不必关心interrupt 以及interrupt handling 的具体实现，因为Webkit建筑在GUI Toolkit之上，而GUI Toolkit已经把底层的interrupt handling，严密地封装起来。Webkit只需要调用GUI Toolkit 的相关APIs，就可以截获鼠标的点击和移动，键盘的输入等等诸多事件。所以，本文着重讨论Figure 2 中，位于顶部的Webkit和JavaScript两层。

不同的操作系统，有相应的GUI Toolkit。GUI Toolkit提供一系列APIs，方便应用程序去管理各色窗口和控件，以及鼠标和键盘等等UI事件的截获和响应。

1. 微软的Windows操作系统之上的GUI Toolkit，是MFC(Microsoft Fundation Classes)。
2. Linux操作系统GNOME环境的GUI Toolkit，是GTK+.
3. Linux KDE环境的，是QT。
4. Java的GUI Toolkit有两个，一个是Sun Microsystem的Java Swing，另一个是IBM Eclipse的SWT。

Swing对native的依赖较小，它依靠Java 2D来绘制窗口以及控件，而Java 2D对于native的依赖基本上只限于用native library画点画线着色。 SWT对native的依赖较大，很多人把SWT理解为Java通过JNI，对MFC，GTK+和QT进行的封装。这种理解虽然不是百分之百准确，但是大 体上也没错。

有了GUI Toolkit，应用程序处理鼠标和键盘等等UI事件的方式，就简化了许多，只需要做两件事情：

1. 把事件来源(event source)，与事件处理逻辑(event listener) 绑定。
2. 解析并执行事件处理逻辑。

Figure 3 显示的是Webkit如何绑定event source和event listener。Figure 4 显示的是Webkit如何调用JavaScript Engine，解析并执行事件处理逻辑。首先看看event source，注意到在HTML文件里有这么一句，

> &lt;img **onclick=”myfunction(‘World’)”** height=”250″ width=”290″ src=”…/antarctica\_mountain\_mirrored.jpg”&gt;

这句话里“&lt;img&gt;”标识告诉Webkit，需要在浏览器页面里摆放一张照片，“src”属性明确了照片的来源，“height, width”明确了照片的尺寸。

onclick”属性提醒Webkit，当用户把鼠标移动到照片显示的区域，并点击鼠标时(onclick)，需要有所响应。响应的方式定义在“onclick”属性的值里面，也就是“myfunction(‘World’)”。

当Webkit解析这个HTML文件时，它依据这个HTML文件生成一棵DOM Tree，和一棵Render Tree。对应于这一句&lt;img&gt;语句，在DOM Tree里有一个HTMLElement节点，相应地，在Render Tree里有一个RenderImage节点。在layout() 过程结束后，根据&lt;img&gt;语句中规定的height和width，确定了RenderImage的大小和位置。由于 Render Tree的RenderImage节点，与DOM Tree的HTMLElement节点一一对应，所以HTMLElement节点所处的位置和大小也相应确定。

因为onclick事件与 这个HTMLElement节点相关联，所以这个HTMLElement节点的位置和大小确定了以后，点击事件的触发区域也就自动确定。假如修改了HTML文件，替换了照片，经过layout() 过程以后，新照片对应的HTMLElement节点，它的位置和大小也自动相应变化，所以，点击事件的触发区域也就相应地自动变化。

在onclick属性的值里，定义了如何处理这个事件的逻辑。有两种处理事件的方式，1. 直接调用HTML DOM method，2. 间接调用外设的Script。onclick=”alert(‘Hello’)”，是第一种方式。alert()是W3C制订的标准的 HTML DOM methods之一。除此以外，也有稍微复杂一点的methods，譬如可以把这一句改成，&lt;img onclick=”document.write(‘Hello’)”&gt;。本文的例子，onclick=”myfunction(‘world’)”，是第二种方式，间接调用外设的Script。

外设的script有多种，最常见的是JavaScript，另外，微软的VBScript和Adobe的ActionScript，在一些浏览器里也能用。即便是JavaScript，也有多种版本，各个版本之间，语法上存在一些差别。为了消弭这些差别，降低JavaScript使用者，以及 JavaScript Engine开发者的负担，ECMA(欧洲电脑产联)试图制订一套标准的JavaScript规范，称为ECMAScript。

各个浏览器使用的JavaScript Engine不同。

1. 微软的IE浏览器，使用的JavaScript Engine是JScript Engine，渲染机是Trident。
2. Firefox浏览器，使用的JavaScript Engine是TraceMonkey，TraceMonkey的前身是SpiderMonkey，渲染机是Gecko。TraceMonkey JavaScript Engine借用了Adobe的Tamarin的部分代码，尤其是Just-In-Time即时编译机的代码。而Tamarin也被用在Adobe Flash的Action Engine中。
3. Opera浏览器，使用的JavaScript Engine是Futhark，它的前身是Linear\_b，渲染机是Presto。
4. Apple的Safari浏览器，使用的JavaScript Engine是SquirrelFish，渲染机是Webkit。
5. Google的Chrome浏览器，使用的JavaScript Engine是V8，渲染机也是Webkit。
6. Linux的KDE和GNOME环境中可以使用Konqueror浏览器，这个浏览器使用的JavaScript Engine是JavaScriptCore，前身是KJS，渲染机也是Webkit。

同样是Webkit渲染机，可以调用不同的JavaScript Engine。之所以能做到这一点，是因为Webkit的架构设计，在设置JavaScript Engine的时候，利用代理器，采取了松散的调用方式。

[![Figure 3. The listener binding of Webkit ](https://s3.ifanr.com/wp-content/uploads/2010/08/2_thumb2.jpg "Figure 3. The listener binding of Webkit ")](https://s3.ifanr.com/wp-content/uploads/2010/08/25.jpg)

*Figure 3. The listener binding of Webkit*

Figure 3 详细描绘了Webkit 设置JavaScript Engine 的全过程。在Webkit 解析HTML文件，生成DOM Tree 和Render Tree 的过程中，当解析到 &lt;img onclick=”…” src=”…”&gt; 这一句的时候，生成DOM Tree中的 HTMLElement 节点，以及Render Tree 中 RenderImage 节点。如前文所述。在生成HTMLElement 节点的过程中，因为注意到有onclick属性，Webkit决定需要给 HTMLElement 节点绑定一个 EventListener，参见Figure 3 中第7步。  
Webkit 把所有EventListener 的创建工作，交给Document 统一处理，类似于 Design Patterns中，Singleton 的用法。也就是说，DOM Tree的根节点 Document，掌握着这个网页涉及的所有EventListeners。 有趣的是，当Document 接获请求后，不管针对的是哪一类事件，一律让代理器 (kjsProxy) 生成一个JSLazyEventListener。之所以说这个实现方式有趣，是因为有几个问题需要特别留意，

1. 一个HTMLElement节点，如果有多个类似于onclick的事件属性，那么就需要多个相应的EventListener object instances与之绑定。
2. 每个节点的每个事件属性，都对应一个独立的EventListener object instance。不同节点不共享同一个 EventListener object instance。即便同一个节点中，不同的事件属性，对应的也是不同的EventListener object instances。  
    这是一个值得商榷的地方。不同节点不同事件对应彼此独立的EventListener object instances，这种做法给不同节点之间的信息传递，造成了很大障碍。反过来设想一下，如果能够有一种机制，让同一个object instance，穿梭于多个HTMLElement Nodes之间，那么浏览器的表现能力将会大大增强，届时，将会出现大量的前所未有的匪夷所思的应用。
3. DOM Tree的根节点，Document，统一规定了用什么工具，去解析事件属性的值，以及执行这个属性值所定义的事件处理逻辑。如前文所述，事件属性的值，分成HTML DOM methods 和JavaScript 两类。但是不管某个HTMLElement节点的某个事件属性的值属于哪一类，Document一律让 kjsProxy代理器，生成一个 EventListener。  
    看看这个代理器的名字就知道，kjsProxy生成的 EventListener，一定是依托JavaScriptCore Engine，也就是以前的KJS JavaScript Engine，来执行事件处理逻辑的。核实一下源代码，这个猜想果然正确。
4. 如果想把JavaScriptCore 替换成其它JavaScript Engine，例如Google的V8，不能简单地更改configuration file，而需要修改一部分源代码。所幸的是，Webkit的架构设计相当清晰，所以需要改动部分不多，关键部位是把Document.{h,cpp} 以及其它少数源代码中，涉及kjsProxy 的部分，改成其它Proxy即可。
5. kjsProxy 生成的EventListener，是JSLazyEventListener。解释一下JSLazyEventListener 命名的寓意，JS容易理解，意思是把事件处理逻辑，交给JavaScript engine 负责。所谓 lazy 指的是，除非用户在照片显示区域点击了鼠标，否则，JavaScript Engine 不主动处理事件属性的值所规定的事件处理逻辑。  
    与 lazy做法相对应的是JIT即时编译，譬如有一些JavaScript Engine，在用户尚没有触发任何事件以前，预先编译了所有与该网页相关的JavaScript，这样，当用户触发了一个特定事件，需要调用某些 JavaScript functions时，运行速度就会加快。当然，预先编译会有代价，可能会有一些JavaScript functions，虽然编译过了，但是从来没有被真正执行过。

[![Figure 4. The event handling of Webkit ](https://s3.ifanr.com/wp-content/uploads/2010/08/4_thumb2.jpg "Figure 4. The event handling of Webkit ")](https://s3.ifanr.com/wp-content/uploads/2010/08/44.jpg) *Figure 4. The event handling of Webkit*

当解析完HTML文件，生成了完整的DOM Tree 和Render Tree 以后，Webkit就准备好去响应和处理用户触发的事件了。响应和处理事件的整个流程，如Figure 4所描述。整个流程分成两个阶段，

1\. 寻找 EventTargetNode。

当用户触发某个事件，例如点击鼠标，根据鼠标所在位置，从Render Tree的根节点开始，一路搜索到鼠标所在位置对应的叶子节点。Render Tree根节点对应的是整个浏览器页面，而叶子节点对应的区域面积最小。

从Render Tree根节点，到叶子节点，沿途每个Render Tree Node，都对应一个DOM Tree Node。这一串DOM Tree Nodes中，有些节点响应用户触发的事件，另一些不响应。例如在本文的例子中，&lt;body&gt; tag 对应的DOM Tree Node，和第一张照片的&lt;img&gt; tag 对应的DOM Tree Node，都对onclick事件有响应。

第一阶段结束时，Webkit得到一个EventTargetNode，这个节点是一个DOM Tree Node，而且是对事件有响应的DOM Tree Node。如果存在多个DOM Tree Nodes对事件有响应，EventTargetNode是那个最靠近叶子的中间节点。

2\. 执行事件处理逻辑。

如果对于同一个事件，有多个响应节点，那么JavaScript Engine 依次处理这一串节点中，每一个节点定义的事件处理逻辑。事件处理逻辑，以字符串的形式定义在事件属性的值中。在本文的例子中，HTML文件包 含&lt;img onclick=”myfunction(‘World’)”&gt;，和&lt;body onclick=”myfunction(‘Hello’)”&gt;，这意味着，有两个DOM Tree Nodes 对onclick事件有响应，它们的事件处理逻辑分别是myfunction(‘World’) 和myfunction(‘Hello’)，这两个字符串。

当JavaScript Engine 获得事件处理逻辑的字符串后，它把这个字符串，根据JavaScript的语法规则，解析为一棵树状结构，称作Parse Tree。有了这棵Parse Tree，JavaScript Engine就可以理解这个字符串中，哪些是函数名，哪些是变量，哪些是变量值。理解清楚以后，JavaScript Engine 就可以执行事件处理逻辑了。本文例子的事件处理过程，如Figure 4中第16步，到第35步所示。  
本文的例子中，“myfunction(‘World’)” 这个字符串本身并没有定义事件处理逻辑，而只是提供了一个JavaScript函数的函数名，以及函数的参数的值。当JavaScript Engine 得到这个字符串以后，解析，执行。执行的结果是得到函数实体的代码。函数实体的代码中，最重要的是alert(v) 这一句。JavaScript Engine 把这一句解析成Parse Tree，然后执行。

注意到本文例子中，对于同一个事件onclick，有两个不同的DOM Tree Nodes 有响应。处理这两个节点的先后顺序要么由capture path，要么由bubbling path决定，如Figure 5所示。(Figure 5中对应的HTML文件，不是本文所引的例子)。在HTML文件中，可以规定event.bubbles属性。如果没有规定，那就按照bubbling的 顺序进行，所以本文的例子，是先执行&lt;img&gt;，弹出“World” 的窗口，关掉“World”窗口后，接着执行&lt;body&gt;，弹出“Hello” 的窗口。

[![Figure 5. The capture and bubbling of event by the DOM tree. ](https://s3.ifanr.com/wp-content/uploads/2010/08/5_thumb.png "Figure 5. The capture and bubbling of event by the DOM tree. ")](https://s3.ifanr.com/wp-content/uploads/2010/08/5.png)

*Figure 5. The capture and bubbling of event by the DOM tree.*

这一节比较枯燥，因为涉及了太多的源代码细节。之所以这么不厌其烦地说明细节，是为了解决如何更有效率地处理事件，以及提供更丰富的手段去处理事件。待续。
