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

公司

2009-06-20 00:41

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 的人,估计都会写。但是耳熟能详,未必等于深入了解。不妨反问自己几个问题,

  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

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 文件里有这么一句,

<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 不同。

  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

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。之所以说这个实现方式有趣,是因为有几个问题需要特别留意,

  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 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.

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

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

登录,参与讨论前请先登录

评论在审核通过后将对所有人可见

正在加载中

移动互联网的围观者、起哄者、以及肇事者。

本篇来自栏目

解锁订阅模式,获得更多专属优质内容