现在的位置: 首页 > 综合 > 正文

WebKit,鼠标引发的故事

2014年01月03日 ⁄ 综合 ⁄ 共 7912字 ⁄ 字号 评论关闭

Figure 1. JavaScript onclick event
Courtesy

http://farm4.static.flickr.com/3302/3640149734_3268bf297f_o.jpg

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

<html>
  <scripttype="text/javascript">
    functionmyfunction(v)
    {
     alert(v)
    }
  </script>

  <bodyonclick="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的元素属性(Elementattribute),还是JavaScript的事件侦听器(EventListener)?换而言之,当用户点击鼠标以后,负责处理onclick事件的,是Webkit 还是JavaScript Engine?

4. Alert()是HTML定义的方法,还是JavaScript提供的函数?谁负责生成那两个弹出的窗口,是Webkit还是JavaScriptEngine?

5.注意到有两个onclick="myfunction(...)",当用户在第一张照片里点击鼠标的时候,为什么是先后弹出,而不是同时弹出?

6.除了PC上的浏览器以外,手机是否也可以完成同样的事件及其响应?假如手机上没有鼠标,但是有触摸屏,如何把onclick定义成用手指点击屏幕?

7. 为什么需要深入了解这些问题? 除了满足好奇心以外,还有没有其它目的?

[转载]新时代新潮流WebOS <wbr>【22】WebKit,鼠标引发的故事

Figure 2. Event callback stacks
Courtesy

http://farm4.static.flickr.com/3611/3640149728_bc64397f60_o.gif

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

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

1. 微软的Windows操作系统之上的GUI Toolkit,是MFC(Microsoft FundationClasses)。

2. Linux操作系统GNOME环境的GUI Toolkit,是GTK+.

3. Linux KDE环境的,是QT。

4. Java的GUI Toolkit有两个,一个是Sun Microsystem的Java Swing,另一个是IBMEclipse的SWT。
 

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

有了GUI Toolkit,应用程序处理鼠标和键盘等等UI事件的方式,就简化了许多,只需要做两件事情。1. 把事件来源(eventsource),与事件处理逻辑(event listener) 绑定。2. 解析并执行事件处理逻辑。

Figure 3 显示的是Webkit如何绑定event source和event listener。Figure 4显示的是Webkit如何调用JavaScript Engine,解析并执行事件处理逻辑。首先看看eventsource,注意到在HTML文件里有这么一句,
   <imgonclick="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,和一棵RenderTree。对应于这一句<img>语句,在DOMTree里有一个HTMLElement节点,相应地,在Render Tree里有一个RenderImage节点。在layout()过程结束后,根据<img>语句中规定的height和width,确定了RenderImage的大小和位置。由于Render Tree的RenderImage节点,与DOMTree的HTMLElement节点一一对应,所以HTMLElement节点所处的位置和大小也相应确定。

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

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

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

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

1. 微软的IE浏览器,使用的JavaScript Engine是JScript Engine,渲染机是Trident。

2. Firefox浏览器,使用的JavaScriptEngine是TraceMonkey,TraceMonkey的前身是SpiderMonkey,渲染机是Gecko。TraceMonkeyJavaScriptEngine借用了Adobe的Tamarin的部分代码,尤其是Just-In-Time即时编译机的代码。而Tamarin也被用在AdobeFlash的Action Engine中。

3. Opera浏览器,使用的JavaScriptEngine是Futhark,它的前身是Linear_b,渲染机是Presto。

4. Apple的Safari浏览器,使用的JavaScriptEngine是SquirrelFish,渲染机是Webkit。

5. Google的Chrome浏览器,使用的JavaScript Engine是V8,渲染机也是Webkit。

6. Linux的KDE和GNOME环境中可以使用Konqueror浏览器,这个浏览器使用的JavaScriptEngine是JavaScriptCore,前身是KJS,渲染机也是Webkit。

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

[转载]新时代新潮流WebOS <wbr>【22】WebKit,鼠标引发的故事

Figure 3. The listener binding of Webkit
Courtesy

http://farm4.static.flickr.com/3659/3640149732_e55446f6b3_b.jpg

Figure 3 详细描绘了Webkit 设置JavaScript Engine 的全过程。在Webkit解析HTML文件,生成DOM Tree 和Render Tree 的过程中,当解析到 <imgonclick="..." src="..."> 这一句的时候,生成DOM Tree中的HTMLElement 节点,以及Render Tree 中 RenderImage 节点。如前文所述。在生成HTMLElement节点的过程中,因为注意到有onclick属性,Webkit决定需要给 HTMLElement
节点绑定一个EventListener,参见Figure 3 中第7步。

Webkit 把所有EventListener 的创建工作,交给Document 统一处理,类似于 DesignPatterns中,Singleton 的用法。也就是说,DOM Tree的根节点Document,掌握着这个网页涉及的所有EventListeners。 有趣的是,当Document接获请求后,不管针对的是哪一类事件,一律让代理器 (kjsProxy)生成一个JSLazyEventListener。之所以说这个实现方式有趣,是因为有几个问题需要特别留意,

1. 一个HTMLElement节点,如果有多个类似于onclick的事件属性,那么就需要多个相应的EventListenerobject instances与之绑定。

2. 每个节点的每个事件属性,都对应一个独立的EventListener object instance。不同节点不共享同一个EventListener objectinstance。即便同一个节点中,不同的事件属性,对应的也是不同的EventListener objectinstances。

  这是一个值得商榷的地方。不同节点不同事件对应彼此独立的EventListener objectinstances,这种做法给不同节点之间的信息传递,造成了很大障碍。反过来设想一下,如果能够有一种机制,让同一个objectinstance,穿梭于多个HTMLElementNodes之间,那么浏览器的表现能力将会大大增强,届时,将会出现大量的前所未有的匪夷所思的应用。

3. DOMTree的根节点,Document,统一规定了用什么工具,去解析事件属性的值,以及执行这个属性值所定义的事件处理逻辑。如前文所述,事件属性的值,分成HTMLDOM methods 和JavaScript两类。但是不管某个HTMLElement节点的某个事件属性的值属于哪一类,Document一律让 kjsProxy代理器,生成一个EventListener。

   看看这个代理器的名字就知道,kjsProxy生成的EventListener,一定是依托JavaScriptCore Engine,也就是以前的KJS JavaScriptEngine,来执行事件处理逻辑的。核实一下源代码,这个猜想果然正确。

4. 如果想把JavaScriptCore 替换成其它JavaScriptEngine,例如Google的V8,不能简单地更改configurationfile,而需要修改一部分源代码。所幸的是,Webkit的架构设计相当清晰,所以需要改动部分不多,关键部位是把Document.{h,cpp}以及其它少数源代码中,涉及kjsProxy 的部分,改成其它Proxy即可。

5. kjsProxy生成的EventListener,是JSLazyEventListener。解释一下JSLazyEventListener命名的寓意,JS容易理解,意思是把事件处理逻辑,交给JavaScript engine 负责。所谓 lazy指的是,除非用户在照片显示区域点击了鼠标,否则,JavaScript Engine不主动处理事件属性的值所规定的事件处理逻辑。

   与lazy做法相对应的是JIT即时编译,譬如有一些JavaScriptEngine,在用户尚没有触发任何事件以前,预先编译了所有与该网页相关的JavaScript,这样,当用户触发了一个特定事件,需要调用某些JavaScript functions时,运行速度就会加快。当然,预先编译会有代价,可能会有一些JavaScriptfunctions,虽然编译过了,但是从来没有被真正执行过。

[转载]新时代新潮流WebOS <wbr>【22】WebKit,鼠标引发的故事

Figure 4. The event handling of Webkit
Courtesy

http://farm4.static.flickr.com/3390/3640149730_0c98f0218d_b.jpg

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

1. 寻找 EventTargetNode。

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

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

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

2. 执行事件处理逻辑。

   如果对于同一个事件,有多个响应节点,那么JavaScriptEngine依次处理这一串节点中,每一个节点定义的事件处理逻辑。事件处理逻辑,以字符串的形式定义在事件属性的值中。在本文的例子中,HTML文件包含<imgonclick="myfunction('World')">,和<bodyonclick="myfunction('Hello')">,这意味着,有两个DOM TreeNodes 对onclick事件有响应,它们的事件处理逻辑分别是myfunction('World')和myfunction('Hello'),这两个字符串。

   当JavaScript Engine获得事件处理逻辑的字符串后,它把这个字符串,根据JavaScript的语法规则,解析为一棵树状结构,称作ParseTree。有了这棵Parse Tree,JavaScriptEngine就可以理解这个字符串中,哪些是函数名,哪些是变量,哪些是变量值。理解清楚以后,JavaScript Engine就可以执行事件处理逻辑了。本文例子的事件处理过程,如Figure 4中第16步,到第35步所示。

   本文的例子中,“myfunction('World')"这个字符串本身并没有定义事件处理逻辑,而只是提供了一个JavaScript函数的函数名,以及函数的参数的值。当JavaScriptEngine 得到这个字符串以后,解析,执行。执行的结果是得到函数实体的代码。函数实体的代码中,最重要的是alert(v)这一句。JavaScript Engine 把这一句解析成Parse Tree,然后执行。

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

[转载]新时代新潮流WebOS <wbr>【22】WebKit,鼠标引发的故事

Figure 5. The capture and bubbling of event by the DOM tree.
Courtesyhttp://www.w3.org/TR/DOM-Level-3-Events/images/eventflow.png

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

抱歉!评论已关闭.