IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Web development | Java technology  >

JavaScript 中的有限状态机,第 3 部分: 测试小部件

用 JavaScript 和有限状态机开发浏览器应用程序

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Edward J Pring (pring@us.ibm.com), 高级软件工程师, IBM

2007 年 3 月 27 日

在本系列中,学习如何用有限状态机系统化地为一个简单 Web 小部件(一个淡入和淡出视图的动画式工具提示)设计复杂的行为。产生的代码既紧凑又简洁,它的逻辑是透明的,它的动画效果即使在负载很重的处理器上也能够顺畅地展现。在本文中,学习如何处理让这个实现能够在所有流行的 Web 浏览器上运行的实际问题。

第 1 部分 讲解如何用有限状态机系统化地为一个简单的 Web 部件设计复杂的行为。第 2 部分 描述了如何用 JavaScript 实现这种行为,并充分利用语言的独特特性,包括关联数组和函数闭包。

有限状态机很早就已用作设计和实现事件驱动的程序内复杂行为的组织原则。现在,可编程的 Web 浏览器为新一代的应用程序开辟了一种全新的事件驱动环境。基于浏览器的应用程序因 Ajax 而广为流行,而同时也变得更为复杂。程序设计人员和实现人员能够大大受益于有限状态机的原理和结构。

本系列的 第 1 部分 描述 Web 页面的一个工具提示部件,与流行的 Web 浏览器提供的内置实现相比,它具有更精致的行为。这种行为要求 FadingTooltip 部件能够响应各种不同的事件。有时候,对事件的响应取决于以前发生的事件。我们使用有限状态机模式设计了这种行为。产生的状态图和 表示指明了所有情况下响应每个事件所需的操作。还确定了需要在事件之间记住的一系列变量,从而支持执行相关操作。

第 2 部分 将设计转换为 JavaScript 代码,并充分利用关联数组和函数闭包功能。这个实现可以适应最流行的浏览器,不需要为浏览器的怪毛病而牺牲效率或优雅性。实现了以下功能的代码:针对所有三种浏览器事件模型连接鼠标事件,启动和取消两种类型的计时器,并连接它们的计时器事件。将状态表实现为适用于所有事件的通用处理函数,并为所有操作和转换提供一个函数数组。这个工具提示是一个完全参数化的 HTML Division 元素,按照状态表中指定的条件,它会随着鼠标事件和计时器事件移动和淡入/淡出。

在最后这篇文章中,将在一些流行的浏览器中对这个实现进行测试。需要构造一个简单的测试页面,它创建一些 FadingTooltip 部件并将它们绑定到 HTML 元素。为了进行对比,测试页面还提供一些内置的工具提示。您很快就会遇到一些不应该发生的 情况,正好借此机会看看设计如何妥善地适应这些情况。本文最后要考察一下性能,并对用有限状态机进一步开发基于浏览器的应用程序提供一些思路。

在浏览器中运行应用程序

在理想情况下,应该在所有可能出现的执行环境中对应用程序进行测试。对于 JavaScript 应用程序来说,由于可用浏览器的种类非常多,广泛使用的版本也非常多,所以进行全面测试是非常困难的。因为 FadingTooltip 部件只是用作技术演示,并不打算在本系列之外的地方使用,所以我只针对四种流行的浏览器的当前版本进行了测试:

这些浏览器的下载链接参见 参考资料
  • Netscape Navigator 8.1
  • Microsoft® Internet Explorer® 6.0
  • Opera 9.0
  • Mozilla Firefox 1.5

我只用下一节描述的简单测试手段测试了这个部件。生产性的应用程序应该进行更全面的测试。





回页首


简单测试手段

对实现进行测试的简单方法之一是在 HTML Web 页面中嵌入一些测试代码。代码必须用构造函数创建 FadingTooltip 对象,并将它们绑定到 HTML 元素。简单的实现方法是利用一个函数,这个函数在 Web 页面的 HTML head 元素中定义,它使用 HTML 元素的 id 属性进行绑定,如清单 1 所示。


清单 1. 创建 FadingTooltip 部件的 JavaScript 代码
				
<head>
    ...
    <script src='FadingTooltip.js' content='text/javascript'></script>
    <script content='text/javascript'>
        function createFadingTooltip(id, content, parameters) {
            new FadingTooltip(document.getElementById(id), content, parameters);
        }
    </script>
    ...
</head>

createFadingTooltip 函数的自变量是一个 HTML 元素标识符、工具提示的内容和一组可选的参数。这个函数简单地将元素标识符转换为一个指针,然后调用构造函数,将其他参数不加修改地传递给构造函数。构造函数返回的对象的指针被丢弃,因为构造函数用它定义的事件函数封装了对象指针,细节参见 第 2 部分 中的 连接鼠标事件 一节。

接下来,需要在 Web 页面的 HTML body 元素中定义一些具有 id 属性的 HTML 元素,如清单 2 所示。


清单 2. 一些 HTML 元素示例的 HTML 代码
				
<body>
    ...
    <p>These elements have tooltips defined with the FadingTooltip widget:
    <div id='tests' class='TestStyle'>
        Here are some <span id='TestLabel'>more elaborate tooltips</span>: 
        <input type='text' id='TestInput' size=25>
        <input type='button' id='TestButton' value='Press this button'>
    </div>
    ...

最后,需要一些用适合每个 HTML 元素的自变量调用 createFadingTooltip 函数的代码,如清单 3 所示。


清单 3. 将 FadingTooltip 部件绑定到 HTML 元素的 JavaScript 代码
				
<body>
    ...
    <script content='text/javascript'>
        createFadingTooltip('TestLabel', 
                         'Move your cursor a bit to the right, please ...');
        createFadingTooltip('TestInput', 
                         'Type the following, <i>please</i>:' +
                         '<ul compact style='margin-top: 0; margin-bottom: 0'>' +
                         '<li>your bank account number' + 
                         '<li>your PIN number' +
                         '</ul>' +
                         '<i>Thank you</i> in advance.', 
                         { fadeinTime: 3,
                           fadeoutTime: 3 } );
        createFadingTooltip('TestButton', 
                         '<img src='smiley.gif' align='absmiddle'>' +
                          '<big>Go ahead.</big> ' +
                          'Press it. ' +
                          '<small>What's the harm? <small>Trust me.</small></small>', 
                          { tooltipOpacity: 1, 
                            tooltipClass: 'AnotherTestStyle',
                            pauseTime: 2,
                            fadeinTime: 0.5, 
                            displayTime: 0, 
                            fadeoutTime: 10,
                            trace: true } );
    /script>
    ...

第一个工具提示用在标识符为 TestLabel 的 HTML 元素上,只需用最简单的自变量来创建:内容是纯文本,并忽略参数自变量,因此对所有参数使用默认值。第二个工具提示用在 TestInput HTML 元素上,它的内容需要用一些标记进行格式化,并指定淡入和淡出时间(以秒为单位)。第三个工具提示用在 TestButton HTML 元素上,在它的格式化内容中包含一个图像,并指定更多的参数,包括一个对工具提示应用样式的 Cascading Stylesheet(CSS)类。

Mozilla Firefox 是最新颖的浏览器,而且比其他浏览器更接近开放标准,所以我们先从它开始。(如果使用 Microsoft Internet Explorer,那么可以跳过这一段,阅读关于 不应该发生的情况淡入/淡出不起作用 的两节,然后再返回这里。)

现在进行 测试。测试页面包含一些具有内置工具提示的 HTML 元素,还包含上面描述的 FadingTooltip 的示例,所以可以对比它们的效果。您会注意到 FadingTooltip 的行为更加精致。它们淡入和淡出视图,而不是直接跳出和消失,而且当鼠标进入和离开 HTML 元素时淡化方向相反。它们会随鼠标移动,当键盘事件发生时不消失。它们具有更精致的外观:FadingTooltip 具有样式,它们的文本进行了格式化,还可以包含图像。

您可以自己判断这些差异是否有改进,是否值得花时间和精力来开发自己的工具提示部件。如果想开发自己的工具提示部件,那么可以将这个实现的源文件和测试页面复制到硬盘上,然后就可以修改参数、样式或代码(参见 下载)。开发时不需要自己的 Web 服务器;只要使用 file://... URL 将修改后的测试页面加载到浏览器中即可。





回页首


当出现不应该发生的事件时

到目前为止,Microsoft Internet Explorer 是使用最广泛的浏览器,所以也必须在 IE 中对实现进行测试,而且用不了多久就能发现问题。当鼠标经过具有 FadingTooltip 部件的任何 HTML 元素时,立刻就会出现一个警告,如图 1 所示。


图 1. Internet Explorer 中出现意外的 mousemove 事件
Internet Explorer 中出现意外的 mousemove 事件

参见 第 1 部分完成状态表 中的状态表(重新显示在图 2 中)。我们没有预料到在 Inactive 状态中出现 mousemove 事件,所以在 第 2 部分创建操作/转换表 中的 actionTransitionFunctions 表实现中这个单元格是空的。


图 2. FadingTooltip 部件的初始状态表
FadingTooltip 部件的初始状态表

从直觉上看,在 mousemove 事件前面应该出现 mouseover 事件,这会导致转移到 Pause 状态,而 Pause 状态是可以处理 mousemove 事件的(参见第 1 部分中的 图 5)。显然,Firefox 符合我们的期望,但是 Internet Explorer 不符合期望。有限状态机在 Internet Explorer 中处理鼠标事件时可能出现 bug,或者浏览器在内部无法跟踪它的状态。在这些情况下,必须在自己的有限状态机中应付这种情况。

幸运的是,应用有限状态机方法很容易解决这样的问题。考虑一下在 Inactive 状态下发生 mousemove 事件时需要什么行为:操作和转换与 Inactive 状态下的 mouseover 事件一样。回顾一下 actionTransitionFunctions 的实现,doActionTransition 方法允许表中的任何函数复制另一个函数的操作和转换。通过使用这个方法,可以在 actionTransitionFunctions 表中添加清单 4 中粗体的函数,从而处理这个意外的事件。


清单 4. 处理 Inactive 状态下意外的 mousemove 事件的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
    ...
    Inactive: {
        mousemove: function(event) {
			return this.doActionTransition('Inactive', 'mouseover', event);
		},
       ...

用 Internet Explorer 进行进一步测试,很快就会发现另一个相似的意外情况,如图 3 所示。


图 3. Internet Explorer 中意外的 mouseout 事件
Internet Explorer 中意外的 mouseout 事件

同样,从直觉上看,mouseout 事件之前应该发生 mouseover 事件,所以 mouseout 事件不应该出现在 Inactive 状态中。幸运的是,这个意外事件也很容易处理。在这种情况下,不需要操作或转换;只需让有限状态机忽略 Inactive 状态中的 mouseout 事件,如清单 5 所示。


清单 5. 处理 Inactive 状态下意外的 mouseout 事件的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
    ...
    Inactive: {
	mouseout: function(event) {
		return this.currentState; // do nothing
	},
      ...

在设计阶段,没有预料到在 Inactive 状态下会出现 mousemovemouseout 事件。但是,在批评 Microsoft 的浏览器中 mouseover 不符合预期之前,我们设想一下任何浏览器在这种情况下会发生什么:鼠标移动到 HTML 元素上并停留足够长的时间,让 FadingTooltip 部件能够淡入视图,显示一段时间,然后淡出视图。这会让有限状态机经历它的所有状态,返回到 Inactive 状态,而鼠标仍然停留在 HTML 元素上。然后,当鼠标移动时,任何浏览器都会在 Inactive 状态下生成 mousemovemouseout 事件。这实际上在其他浏览器中也会发生,包括 Firefox。即使不考虑 Internet Explorer 中的 bug,这个有限状态机中也有一个设计缺陷,如图 4 所示。


图 4. Firefox 中意外的 mousemove 事件
Firefox 中意外的 mousemove 事件

幸运的是,为 Internet Explorer 所做的 Inactive 状态修改可以正确地处理这种情况,所以不需要做进一步修改来克服这个设计缺陷。

不幸的是,实现这些修改之后的后续测试发现 Internet Explorer 有另一个不应该发生的情况:在 Pause 状态下出现意外的 mouseover 事件。

因为 Pause 状态下 mouseover 事件需要的操作和转换与 Inactive 状态下的 mouseover 事件一样,所以可以通过调用 doActionTransaction 方法来处理这种情况。但是,没有合理的事件序列会让其他浏览器进入这种情况(至少我没有发现),所以只为 Internet Explorer 实现这个设计修改,如清单 6 所示。


清单 6. 处理只在 Internet Explorer 中发生的意外事件的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
    },
    ...
};
if ( (window.navigator.userAgent).indexOf('MSIE')!=-1 ) {
		FadingTooltip.prototype.actionTransitionFunctions["Pause"]["mouseover"] = 
		function(event) { return 
		this.doActionTransition('Inactive', 'mouseover', event);
		};
		}

如果浏览器是某个版本的 Internet Explorer,那么修改 FadingTooltips 原型的 actionTransitionFunctions 表(在定义它之后,但在使用它之前),按照与 Inactive 状态下的 mouseover 事件相同的方式处理 Pause 状态下的 mouseover 事件。请记住,在 JavaScript 中,关联数组和对象是等效的,所以可以使用这两种表示法之一修改这个表。





回页首


当淡化工具提示的淡化效果不起作用时

不幸的是,在用前一节中的设计修改消除关于意外事件的所有警告之后,对 Internet Explorer 的进一步测试发现了另一个无关的问题。用 FadingTooltip 部件定义的工具提示会直接弹入和弹出视图,而不是淡入和淡出。不幸的是,Internet Explorer 不支持提议的标准 CSS opacity 样式(参见 参考资料)。

Internet Explorer 支持非标准的样式 filter,这个属性具有相似的功能(参见 参考资料)。为了应用这个属性,需要在 createTooltip 方法中插入清单 7 中粗体的代码行。


清单 7. 在 Internet Explorer 中创建工具提示的额外 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    createTooltip: function() {  
    this.tooltipDivision = document.createElement('div');
        ...   
        this.currentOpacity = this.tooltipDivision.style.opacity = 0;
       if (this.tooltipDivision.filters) { // for MSIE only
				this.tooltipDivision.style.filter = 'alpha(opacity=0)';
				}
        ...
    },	
    ... 

还需要在 fadeTooltip 方法中插入对应的代码,如清单 8 所示。


清单 8. 在 Internet Explorer 中使工具提示产生淡化效果的额外 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    fadeTooltip: function(opacityDelta) { 
        ...
        this.tooltipDivision.style.opacity = this.currentOpacity;
       if (this.tooltipDivision.filters) { // for MSIE only
				this.tooltipDivision.filters.item('alpha').opacity = 
				100*this.currentOpacity;
				}
    },	
    ... 

还有一个要注意的问题,Opera 作为浏览器品牌可能还不够有名气,但很多技术文章都比较关注它。遗憾的是,Opera 在支持 CSS opacity 样式方面还比较落后,所以在 Opera 9 之前的版本中,FadingTooltip 部件也会直接弹入和弹出视图,而不是淡入和淡出。与 Internet Explorer 不同,早期的 Opera 版本没有为透明度提供替代语法;惟一的解决方案是升级到当前版本。





回页首


关于性能的几点说明

FadingTooltip 部件现在可以在流行的浏览器中顺利工作了,现在就该看看处理器使用率方面的性能目标是否可以实现了。在 Windows 上考察性能影响的一种简便方法是,在运行这个部件的动画时,查看 Windows Task Manager 的 Performance 面板。

在大多数工作站上,通常都有几个程序在后台运行,即使在用户没有启动任何应用程序时也是如此。这些程序中的一部分会在 Windows 任务条上的时钟旁边显示小图标;其他程序根本没有可见的运行迹象。但是,Windows Task Manager 的 Performance 面板会显示它们的活动,如图 5 所示。


图 5. Windows Task Manager 显示一般的后台活动
Windows Task Manager 显示一般的后台活动

在运行 FadingTooltip 部件之前,可以取消一部分后台活动,以免它们干扰对这个部件的处理器使用率的观察。可以通过上下文菜单关闭大多数在 Windows 任务条上显示图标的程序。可以通过 Windows Control Panel for Services 停止其他后台程序。

在具有 1.1GHz Intel Pentium-III 处理器的系统上,如果没有什么其他负载,FadingTooltip 部件的处理器使用率看起来确实很低,如图 6 所示。


图 6. Windows Task Manager 显示部件动画活动
Windows Task Manager 显示部件动画活动

因为这个部件的动画对处理器的需求很低,所以可以认为它即使在负载很重的处理器上也能够顺畅地完成。





回页首


不要忘记更新设计文档

既然已经对实现进行了测试,就需要更新文档来反映设计和实现的变化。遇到的意外事件对应于状态表中的空单元格,所以需要更新这些单元格(以蓝色突出显示),添加上处理这些情况的操作,如图 7 所示。


图 7. 测试后的 FadingTooltip 部件状态表
测试后的 FadingTooltip 部件状态表

如图 8 所示,对状态图进行相应的修改也同样简单。


图 8. 测试后的 FadingTooltip 部件状态图
测试后的 FadingTooltip 部件状态图

createTooltipfadeTooltip 方法中插入的代码行处理 Internet Explorer 对待不透明度的特殊方式,这些代码实际上不算是设计修改。可以在源代码注释中记录这些修改(参见 下载)。





回页首


进一步开发

本系列的目的是演示如何将有限状态机设计模式应用于基于浏览器的应用程序,以及如何利用 JavaScript 语言的两个独特特性开发优雅高效的程序。因此,将 FadingTooltip 部件开发成一个自包含的 JavaScript 对象。代码很简洁,但是不太灵活。在结束讨论之前,我想谈谈进一步开发的一些可能的方向。

其他可见部件也可以受益于与 FadingTooltip 部件相似的外观和行为。例如,对于输入字段中的输入错误,可以显示语法错误消息;对于复杂表单的填写提供建议;在用户不熟悉的工作流中提供指导。这些都可以用与工具提示相似的框来实现,这些提示框在发生适当事件时淡入视图,随鼠标移动,最终在事件许可时淡出视图。一个可能的开发步骤是对 FadingTooltip 代码进行重构,将有限状态机的基本机制转移到一个单独的对象中。这可能包括事件关联函数、事件处理函数、错误方法和计时器方法。然后,可以将一系列可见部件(包括 FadingTooltip 部件)实现为单独的对象,让它们继承基本对象的数据和方法,并添加每种部件所需的状态变量、转换表和操作方法。

与工具提示相似的部件和其他可见部件可能受益于与淡化相似的附加动画效果。例如,工具提示可以从窗口的边缘滑入视图,或者从一个点放大,然后消失于一点,或者像折纸一样从图标开始在视图中展开,或者在鼠标经过时像果冻一样颤动。这些动画效果也可以应用于上下文菜单、对话框和输入提示框。另一个可能的开发步骤是为可见部件构建一个框架,将每种动画的有限状态机和每种部件的行为进一步分开放入单独的对象,这样就能够在特殊 HTML 元素上绑定任何组合。

当然,我们没有在这些文章中考虑其他类型的事件,比如键盘事件或网络事件。例如,在文本输入字段中输入各个字符时,可以触发操作对输入的值进行校验,或者在一个选择列表中显示留下的有效值。这些操作都可以基于用有限状态机表示的形式语法。可以通过重新试用协议图表示的替换服务来处理网络请求失败或超时。其他的进一步开发步骤可能涉及:

  • 扩展有限状态机的框架,包含键盘和网络事件的关联函数
  • 形式语言语法和网络协议的图或表表示法
  • 校验文本值和管理网络会话的操作

在同一处理器上运行的其他应用程序可以引发事件和公开操作,浏览器中运行的程序可能对这些事件和操作感兴趣。例如,IP 语音电话技术应用程序可能生成 ring-inring-outconnectdisconnect 事件,并公开 callholdhang-up 操作。开发的下一步骤是扩展框架,包含连接其他应用程序中的事件和在其中执行操作的方法。可能需要用一些 Java™ 插件让浏览器中运行的有限状态机可以访问其他进程中的事件和操作。插件是非常适合放置代码的位置,因为 JavaScript 最初就是用于 Java 插件的脚本语言。



下载


参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • Ajax: A New Approach to Web Applications:阅读 Jesse James Garrett 的这篇介绍 Ajax 的大作。

  • JavaScript: The Definitive Guide 一书(David Flanagan,1996 年至 2006 年由 O'Reilly Media 多次再版):获得有关如何能让 JavaScript 在浏览器中运行的详尽信息。

  • Standard ECMA-262: ECMAScript Language Specification(Ecma International,1999):研读可由流行的浏览器实现的 JavaScript 语言的权威定义。

  • W3C Cascading Style Sheets Under Construction:了解 CSS3 的开发和 CSS WG(Cascading Style Sheets Working Group,以前称为 “CSS & FP WG”)活动的大致计划。

  • MSDN Alpha Filter:Alpha Filter 用来调整对象内容的不透明度。

  • Document Object Model (DOM) Level 2 Events Specification(W3C,2000):参考此规范,获取 DOM Level 2 事件模型的权威定义。

  • Gecko DOM Reference (Mozilla):获得由 Firefox 浏览器实现的对象接口(包括事件)的权威定义。

  • HTML and DHTML Reference (Microsoft):参考由 Internet Explorer 浏览器实现的对象接口(包括事件)的权威定义。

  • Computer Network Architectures and Protocols (Paul E. Green 主编,Jr., Plenum Press 出版,1982 年)一书中的第 21 章 “Protocol Representation with Finite State Models”(作者:Andre A. S. Danthine)和第 25 章 “Executable Representation and Validation of SNA”(作者:Gary D. Schultz 等):了解应用于计算机网络协议的有限状态机的一些历史示例。

  • Compilers: Principles, Techniques, ad Tools (Alfred V. Aho 等,Addison-Welsley,1986 年)一书中的第 3.5 章“Finite Automata”描述了如何将有限状态机应用到计算机语言编译器。

  • Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma 等,Addison-Welsley,1995 年)一书中的第 5 章 “Behavioral Patterns” 讨论了实现有限状态机涉及到的状态模式。

  • Internetworking with TCP/IP (Douglas E. Comer,Simon and Schuster Company,1995 年)一书中的第 13.25 章 “TCP State Machine”:了解互联网底层的有限状态机。

  • Unified Modeling Language 2.0 Superstructure Specification (Object Management Group,2004 年)中的第 15 章 “State Machines”:给出了有限状态机的一个完整的图形表示。

  • developerWorks Web 开发专区:通过专注于 Web 技术的文章和教程扩展您的站点开发技能。

  • technology bookstore:浏览有关这些主题和其他技术主题的书籍。

  • developerWorks 技术活动和网络广播:随时关注和了解技术的最新发展,缩短学习过程,改进最困难的软件项目的质量和结果。


获得产品和技术

讨论


关于作者

Photo of Edward Pring

Edward Pring 拥有斯坦福大学的数学专业学士学位和纽约大学的计算机科学硕士学位。作为 IBM 的一名研发人员,他对 IBM 的许多产品和技术 —— 包括操作系统、出版应用程序、大型机的终端仿真器、个人计算机的病毒防护、Digital Immune 系统的网络自动化,以及 Web 服务的可视化和性能分析 —— 都做出了卓越的贡献。他还在这些领域拥有多项技术专利。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款