新时代新潮流 WebOS【22】WebKit,鼠标引发的故事
Figure 1. JavaScript onclick event
先看一段简单的 HTML 文件。在浏览器里打开这个文件,将看到两张照片。把鼠标移动到第一张照片,点击鼠标左键,将自动弹出一个窗口,上书 “World”。 但是当鼠标移动到第二张照片,或者其它任何区域,点击鼠标,却没有反应。关闭 “World” 窗口,自动弹出第二个窗口,上书 “Hello”。
<html>
<script type=”text/javascript”>
function myfunction(v)
{
alert(v)
}
</script>
<body onclick=”myfunction(‘Hello’)”>
<p>
<img onclick=”myfunction(‘World’)” height=”250″ width=”290″ src=”http://www.dirjournal.com/info/wp-content/uploads/2009/02/antarctica_mountain_mirrored.jpg”>
<p>
<img height=”206″ width=”275″ src=”http://media-cdn.tripadvisor.com/media/photo-s/01/26/f4/eb/hua-shan-hua-mountain.jpg”>
</body>
</html>
这段 HTML 文件没有什么特别之处,所有略知一点 HTML 的人,估计都会写。但是耳熟能详,未必等于深入了解。不妨反问自己几个问题,
- 浏览器如何知道,是否鼠标的位置,在第一个照片的范围内?
- 假如修改一下 HTML 文件,把第一张照片替换成另一张照片,前后两张照片的尺寸不同。在浏览器里打开修改后的文件,我们会发现,能够触发弹出窗口事件的区域面积,随着照片的改变而自动改变。浏览器内部,是通过什么样的机制,自动识别事件触发区域的?
- Onclick 是 HTML 的元素属性 (Element attribute),还是 JavaScript 的事件侦听器 (EventListener)?换而言之,当用户点击鼠标以后,负责处理 onclick 事 件的,是 Webkit 还是 JavaScript Engine?
- Alert() 是 HTML 定义的方法,还是 JavaScript 提供的函数?谁负责生成那两个弹出的窗口,是 Webkit 还是 JavaScript Engine?
- 注意到有两个 onclick=”myfunction(…)”,当用户在第一张照片里点击鼠标的时候,为什么是先后弹出,而不是同时弹出?
- 除了 PC 上的浏览器以外,手机是否也可以完成同样的事件及其响应?假如手机上没有鼠标,但是有触摸屏,如何把 onclick 定义成用手指点击屏幕?
- 为什么需要深入了解这些问题? 除了满足好奇心以外,还有没有其它目的?
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 事件的截获和响应。
- 微软的 Windows 操作系统之上的 GUI Toolkit,是 MFC(Microsoft Fundation Classes)。
- Linux 操作系统 GNOME 环境的 GUI Toolkit,是 GTK+.
- Linux KDE 环境的,是 QT。
- 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 事件的方式,就简化了许多,只需要做两件事情:
- 把事件来源 (event source),与事件处理逻辑 (event listener) 绑定。
- 解析并执行事件处理逻辑。
Figure 3 显示的是 Webkit 如何绑定 event source 和 event listener。Figure 4 显示的是 Webkit 如何调用 JavaScript Engine,解析并执行事件处理逻辑。首先看看 event source,注意到在 HTML 文件里有这么一句,
<img onclick=”myfunction(‘World’)” height=”250″ width=”290″ src=”…/antarctica_mountain_mirrored.jpg”>
这句话里 “<img>” 标识告诉 Webkit,需要在浏览器页面里摆放一张照片,“src” 属性明确了照片的来源,“height, width” 明确了照片的尺寸。
onclick” 属性提醒 Webkit,当用户把鼠标移动到照片显示的区域,并点击鼠标时 (onclick),需要有所响应。响应的方式定义在 “onclick” 属性的值里面,也就是 “myfunction(‘World’)”。
当 Webkit 解析这个 HTML 文件时,它依据这个 HTML 文件生成一棵 DOM Tree,和一棵 Render Tree。对应于这一句<img> 语句,在 DOM Tree 里有一个 HTMLElement 节点,相应地,在 Render Tree 里有一个 RenderImage 节点。在 layout() 过程结束后,根据<img> 语句中规定的 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,譬如可以把这一句改成,<img onclick=”document.write(‘Hello’)”>。本文的例子,onclick=”myfunction(‘world’)”,是第二种方式,间接调用外设的 Script。
外设的 script 有多种,最常见的是 JavaScript,另外,微软的 VBScript 和 Adobe 的 ActionScript,在一些浏览器里也能用。即便是 JavaScript,也有多种版本,各个版本之间,语法上存在一些差别。为了消弭这些差别,降低 JavaScript 使用者,以及 JavaScript Engine 开发者的负担,ECMA(欧洲电脑产联) 试图制订一套标准的 JavaScript 规范,称为 ECMAScript。
各个浏览器使用的 JavaScript Engine 不同。
- 微软的 IE 浏览器,使用的 JavaScript Engine 是 JScript Engine,渲染机是 Trident。
- Firefox 浏览器,使用的 JavaScript Engine 是 TraceMonkey,TraceMonkey 的前身是 SpiderMonkey,渲染机是 Gecko。TraceMonkey JavaScript Engine 借用了 Adobe 的 Tamarin 的部分代码,尤其是 Just-In-Time 即时编译机的代码。而 Tamarin 也被用在 Adobe Flash 的 Action Engine 中。
- Opera 浏览器,使用的 JavaScript Engine 是 Futhark,它的前身是 Linear_b,渲染机是 Presto。
- Apple 的 Safari 浏览器,使用的 JavaScript Engine 是 SquirrelFish,渲染机是 Webkit。
- Google 的 Chrome 浏览器,使用的 JavaScript Engine 是 V8,渲染机也是 Webkit。
- Linux 的 KDE 和 GNOME 环境中可以使用 Konqueror 浏览器,这个浏览器使用的 JavaScript Engine 是 JavaScriptCore,前身是 KJS,渲染机也是 Webkit。
同样是 Webkit 渲染机,可以调用不同的 JavaScript Engine。之所以能做到这一点,是因为 Webkit 的架构设计,在设置 JavaScript Engine 的时候,利用代理器,采取了松散的调用方式。
Figure 3. The listener binding of Webkit
Figure 3 详细描绘了 Webkit 设置 JavaScript Engine 的全过程。在 Webkit 解析 HTML 文件,生成 DOM Tree 和 Render Tree 的过程中,当解析到 <img onclick=”…” src=”…”> 这一句的时候,生成 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。之所以说这个实现方式有趣,是因为有几个问题需要特别留意,
- 一个 HTMLElement 节点,如果有多个类似于 onclick 的事件属性,那么就需要多个相应的 EventListener object instances 与之绑定。
- 每个节点的每个事件属性,都对应一个独立的 EventListener object instance。不同节点不共享同一个 EventListener object instance。即便同一个节点中,不同的事件属性,对应的也是不同的 EventListener object instances。
这是一个值得商榷的地方。不同节点不同事件对应彼此独立的 EventListener object instances,这种做法给不同节点之间的信息传递,造成了很大障碍。反过来设想一下,如果能够有一种机制,让同一个 object instance,穿梭于多个 HTMLElement Nodes 之间,那么浏览器的表现能力将会大大增强,届时,将会出现大量的前所未有的匪夷所思的应用。 - DOM Tree 的根节点,Document,统一规定了用什么工具,去解析事件属性的值,以及执行这个属性值所定义的事件处理逻辑。如前文所述,事件属性的值,分成 HTML DOM methods 和 JavaScript 两类。但是不管某个 HTMLElement 节点的某个事件属性的值属于哪一类,Document 一律让 kjsProxy 代理器,生成一个 EventListener。
看看这个代理器的名字就知道,kjsProxy 生成的 EventListener,一定是依托 JavaScriptCore Engine,也就是以前的 KJS JavaScript Engine,来执行事件处理逻辑的。核实一下源代码,这个猜想果然正确。 - 如果想把 JavaScriptCore 替换成其它 JavaScript Engine,例如 Google 的 V8,不能简单地更改 configuration file,而需要修改一部分源代码。所幸的是,Webkit 的架构设计相当清晰,所以需要改动部分不多,关键部位是把 Document.{h,cpp} 以及其它少数源代码中,涉及 kjsProxy 的部分,改成其它 Proxy 即可。
- 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
当解析完 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 中,有些节点响应用户触发的事件,另一些不响应。例如在本文的例子中,<body> tag 对应的 DOM Tree Node,和第一张照片的<img> tag 对应的 DOM Tree Node,都对 onclick 事件有响应。
第一阶段结束时,Webkit 得到一个 EventTargetNode,这个节点是一个 DOM Tree Node,而且是对事件有响应的 DOM Tree Node。如果存在多个 DOM Tree Nodes 对事件有响应,EventTargetNode 是那个最靠近叶子的中间节点。
2. 执行事件处理逻辑。
如果对于同一个事件,有多个响应节点,那么 JavaScript Engine 依次处理这一串节点中,每一个节点定义的事件处理逻辑。事件处理逻辑,以字符串的形式定义在事件属性的值中。在本文的例子中,HTML 文件包 含<img onclick=”myfunction(‘World’)”>,和<body onclick=”myfunction(‘Hello’)”>,这意味着,有两个 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 的 顺序进行,所以本文的例子,是先执行<img>,弹出 “World” 的窗口,关掉 “World” 窗口后,接着执行<body>,弹出 “Hello” 的窗口。
Figure 5. The capture and bubbling of event by the DOM tree.
这一节比较枯燥,因为涉及了太多的源代码细节。之所以这么不厌其烦地说明细节,是为了解决如何更有效率地处理事件,以及提供更丰富的手段去处理事件。待续。