📄 9932.txt
字号:
事件的元素标记中都可以将它作为参数传递给脚本。在这一点上,清单1
和清单2中的实现是完全相同的(应该指出的是,在IE中将事件作为参数
传递并非完全必要,因为所有事件句柄都可以通过查询window.event对
象来确定哪一个事件调用了它)。以相同的方法传递事件参数是这两个
脚本的共同之处。
第三,请注意事件在多个句柄之间的传递方式。在IE下,除非有特
别指定(即设置事件的cancelBubble属性),否则事件就不断向更高一
级传递。也就是说,如果文档内为同一事件指定了多个不同处理句柄,
IE的缺省行为是不断在多个句柄之间传递该事件,直到所有句柄都有机
会执行为止。而在Navigator下则大不相同,事件在不同级别的句柄之间
持续传播需要显式调用routeEvent。此外,在Navigator下调用handleEvent
还可以将事件直接(不是按照一般的分级次序)传递给任意元素。而在
IE下,则可以调用元素的事件方法模仿handleEvent,如:
document.myElement.onmousedown()
最后应该注意的是,两个脚本以完全相反的次序处理事件:对运行
于Navigator的代码来说,click事件先在文档这一级被捕获,然后才是
按钮;而运行于IE的脚本则恰恰相反。
相对而言,Navigator的事件处理方法略显繁琐。当程序调用
captureEvents、系统接受请求之后,脚本就拥有对该事件的完全控制权
了。如果要把事件传递给其它句柄,就必须显式地调用routeEvent。如
上例,bodyEvent捕获鼠标单击事件之后就一直保留对它的控制权,直到
后来调用routeEvent才把事件“释放给”buttonEvent。
如果在IE中创建了一个文档级的事件句柄,可以理解为在文档级创
建了一个事件处理器,它能够处理所有“上传”到这一级的特定类别事件。
只要触发事件的元素不处理它,或虽经初始处理仍允许上传(缺省),事
件就可能被其它的句柄处理。在上例中,单击按钮使得buttonEvent被执
行。在buttonEvent结束之后,系统就自动在更高一级查找是否存在要求
处理该事件的句柄。在文档这一级它找到了符合要求的句柄,于是控制权
就转到bodyEvent。当bodyEvent结束执行,再次开始寻找处理句柄(此时
在窗口一级),但寻找失败,于是结束整个事件处理过程。
通过以上分析可以看出,要求同一脚本在不同浏览器下行为相同,
最大的问题在于两者事件传递次序和实现方法上的不同。由此,我们的解
决方法是:放弃两个浏览器的事件传递模型和事件分级概念,用一个通用
的事件接口取代之。
二、建立统一的事件控制接口
如清单3所示,Events.js实现了一个通用的事件接口。该接口并不
直接在元素与事件之间建立关联,所有的事件均在文档这一级捕获,直到
句柄被调用时才考虑事件的原始来源。而事件的类型(如click或mouseover)
以及触发该事件的元素(如上例中的myButton)可以通过分析事件对象来
获知。这里不再存在“事件级别”这一概念,这是因为对于任意一个指定
事件来说,句柄被调用的次序不再反映元素包含与被包含这一文档中的概
念。事件句柄的调用次序决定于其注册次序。
【清单3 events.js】
// 创建"事件-句柄"列表
// 该列表中登记所有需要捕获的事件
// 而每一个事件又分别对应于一组事件处理句柄
var eventHandler = new eventHandlerArray( null );
// 当前运行环境
var fNetscape = navigator.appName.indexOf( "Microsoft" ) !=-1? false : true;
// 创建公用的事件对象
var xEvent = new xEventObject();
function xEventObject()
{
this.event = null;
this.element = null;
return this;
}
function eventHandlerArray()
{
return new Array( eventHandlerItem( null ) );
}
function eventHandlerItem( handlerID )
{
// 事件处理句柄数组
this.funcList = new Array()
// 事件标识ID(事件名称)
this.hID = handlerID
return this
}
// 在"事件-句柄"列表中登记一组新的事件-句柄
function hookEvent()
{
var theEvent = hookEvent.arguments[0]
var theHook = hookEvent.arguments[1]
// 在文档级捕获指定事件
if ( fNetscape == true )
{
// Navigator: 指定在文档级捕获的事件及其句柄runEvent
if ( document.captureEvents )
{
eval( "document.captureEvents( Event." + theEvent.toUpperCase() + " );" );
eval( "document.on" + theEvent + " = runEvent;" );
}
else return;
}
else
{
// IE: 指定在文档级捕获的事件及其句柄runEvent
if ( !eval( "document.on" + theEvent ) )
eval( "document.on" + theEvent + " = runEvent;" );
}
// 当前事件是否已经登记?
for ( i = 0; eventHandler[ i ]; i ++ )
{
if ( eventHandler[ i ].hID == theEvent )
{
// 当前事件已经登记,为该事件登记新的处理句柄
for ( j = 0; eventHandler[ i ].funcList[ j ] != null; j ++ ) {}
eventHandler[ i ].funcList[ j ] = theHook;
return;
}
}
// 当前事件不在事件-句柄列表中
eventHandler[ i ] = new eventHandlerItem( null );
eventHandler[ i ].hID = theEvent
eventHandler[ i ].funcList[0] = theHook
}
// 公用事件句柄,将捕获的事件转发给所有对应的用户句柄
function runEvent( event )
{
if ( fNetscape == false )
{
xEvent.event = window.event;
xEvent.element = window.event.srcElement;
}
else
{
xEvent.event = event;
xEvent.element = event.target;
}
// 在事件-句柄列表中查找指定事件
for ( i = 0; eventHandler[ i ]; i ++ )
{
if ( eventHandler[ i ].hID == xEvent.event.type )
{
// 依次运行对应于该事件的各个句柄
for ( j = 0; eventHandler[ i ].funcList[ j ]; j ++ )
{
retCode = eval( eventHandler[ i ].funcList[ j ] + "( xEvent )" )
if ( retCode == false )
{
event.cancelBubble = true;
return false;
}
}
// 当前事件的用户句柄运行结束,允许缺省处理
return true;
}
}
return false
}
由前面的讨论过程可知,IE和Navigator在传递事件对象作为脚本参
数、事件对象属性、元素属性这几方面存在共同之处。为实现用统一的方
法使用其共同部分,在Events.js中声明了一个xEvent对象。xEvent包含两
个组成部分——event和element,前者对应于IE的window.event和Navigator
的event;后者对应于IE的window.event.srcElement和Navigator的event.target。
两者共同的事件属性包括type和cancelBubble,元素属性则包括type和name等。
全局变量eventHandler是一个动态分配的eventHandlerItem数组,其
中每一项包含事件名字(即事件ID,如click,mouseover)以及与该事件对
应的句柄数组。每当hookEvent被调用,程序就搜索eventHandler,检查指
定事件是否已经登记。如果是,则新的句柄被加入到已有的句柄数组末尾;
否则,就在eventHandler中创建新的eventHandlerItem项,然后在其中注册
当前事件及对应的事件处理句柄。
所有需要处理事件的脚本均在文档级捕获事件,且将它的事件处理句
柄指定为runEvent。runEvent是一个公用事件句柄,它负责将事件转发给真
正完成处理任务的用户句柄。一旦某个事件在文档级被捕获,则所有属于它
的用户句柄都被依次调用,从而完成事件处理任务。使用该事件接口的基本
过程可以为:
⑴ 在HTML文档的开始部分包含events.js脚本。
⑵ 在用户句柄文件中,调用hookEvent注册所有要求处理的事件,传
递给它的参数是事件名字以及处理该事件的句柄。
⑶ 实现用户句柄。句柄返回真值表示允许其它脚本继续处理同一事件;
否则禁止进一步处理。
抛弃分级概念获得的好处在于:可以自由地在事件、句柄和触发事件
的元素之间建立关系。现在,整个HTML文档可以看成一个事件源,而该接口
则成为一个公用的事件转发器,要求处理某个事件的脚本只需进行适当的注
册并提供处理句柄即可。此外,基于该事件模型的HTML文档不再需要声明
(和实现)基于事件的功能,而是由一组分离的脚本文件实现。触发事件的
元素不再指定任何处理句柄,而是由脚本捕获这些事件及决定处理方法。由
此,采用统一的事件接口也有利于改善代码的结构化和可读性。
三、应用示例
清单4中的hFlipButton.js捕获mouseover、mouseout、click三个事件。
click事件被传递给flipState。对于NAME域包含单词“flip”的元素,
flipState切换其标题为“SLOW”或“FAST”。mouseover事件被传递给
highlightState函数。对于NAME域包含“highlight”的元素,highlightState
设置一个每秒触发三次的时钟,将元素标题的首尾字符改为闪烁的星号。
mouseout事件传递给unhighlightState,用来关闭元素标题的突出显示。此
脚本说明了应用通用事件接口的一般过程,以及在句柄中如何确定事件的原
始来源。
【清单4 hFlipButton.js】
< !-- 声明 FlipButton 要求捕获的事件及其处理句柄 -->
hookEvent( "click", "flipState" )
hookEvent( "mouseover", "highlightState" )
hookEvent( "mouseout", "unhighlightState" )
// 存储正被突出显示的元素
_hiElement = null;
_timer = null;
// 鼠标单击: 切换标题
function flipState( e )
{
if ( e.element.name )
{
if ( e.element.name.toLowerCase().indexOf( "flip" ) != -1 )
{
var str = e.element.value;
if ( str.indexOf( "SLOW" ) != -1 )
e.element.value = str.charAt( 0 ) == "*" ? "*FAST*" : " FAST ";
else
e.element.value = str.charAt( 0 ) == "*" ? "*SLOW*" : " SLOW ";
}
}
return true;
}
// 鼠标进入: 标题突出显示(首尾加闪烁的星号)
function highlightState( e )
{
if ( e.element.name )
{
if ( e.element.name.toLowerCase().indexOf( "highlight" ) != -1 )
{
// 设置当前元素为突出显示
_hiElement = e.element;
// 终止以前的时钟
if ( _timer )
clearTimeout( _timer );
// 启动突出显示过程
doHighlight();
}
}
return true;
}
// 鼠标离开: 恢复标题为非突出显示
function unhighlightState( e )
{
if ( e.element.name )
{
if ( e.element.name.toLowerCase().indexOf( "highlight" ) != -1 )
{
if ( _hiElement != null )
{
str = _hiElement.value;
// 如标题首尾位置含星号,则删除之
if ( str.charAt( 0 ) == "*" )
{
_hiElement.value = " " + str.substring( 1, str.length - 1 ) + " ";
}
}
_hiElement = null;
}
}
return true;
}
function doHighlight()
{
if ( _hiElement != null )
{
var str = _hiElement.value;
// 将首尾字符在" " 和 "*" 之间切换
if ( str.charAt( 0 ) == "*" )
_hiElement.value = " " + str.substring( 1, str.length - 1 ) + " ";
else
_hiElement.value = "*" + str.substring( 1, str.length - 1 ) + "*"
// 设置切换操作的间隔时间
_timer = setTimeout( "doHighlight()", 300 );
}
}
清单5的hStatusWrite.js捕获同样三个事件。但这里mouseover事件由
statusWrite函数处理。statusWrite显示鼠标所经过元素的NAME域,或NAME
属性不存在时显示“未知元素”。mouseout事件由statusCancel处理。
statusCancel在鼠标离开某个元素时清除状态条信息。click事件由changeName
函数处理。对于所有在NAME域包含“mutable”字符串的元素,changeName
响应其click事件,赋予NAME域某个随机值,如“mutable flip”、“mutable
highlight”、“mutable flip highlight”。在完成上述操作之后,
changeName显式地调用第二个句柄,使得对NAME域的改变可以立即反映出来。
此脚本说明通用事件接口动态维护事件句柄的能力。
【清单5 hStatusWrite.js】
< !-- 声明 StatusWrite 要求捕获的事件及其处理句柄 -->
hookEvent( "mouseover", "statusWrite" )
hookEvent( "mouseout", "statusCancel" )
hookEvent( "click", "changeName" )
// 鼠标进入事件: 显示元素名字
function statusWrite( e )
{
if ( e.element.name )
{
window.status = "元素: " + e.element.name
}
else
{
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -