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