JavaScript 中的有限状态机
JavaScript 中的有限状态机,第 1 部分: 设计一个小部件使用 JavaScript 和有限状态机开发浏览器应用程序 |
|
级别: 初级
Edward J Pring (pring@us.ibm.com ), Senior Software Engineer, IBM
2007 年 1 月 29 日
有 限状态机很早就已用作设计和实现事件驱动的程序(比如网络适配器和编译器)内复杂行为的组织原则。现在,可编程的 Web 浏览器为新一代的应用程序开辟了一种全新的事件驱动环境。基于浏览器的应用程序因 Ajax 而广为流行,而同时也变得更为复杂。程序设计人员和实现人员能够大大受益于有限状态机的原理和结构。本篇文章将向您介绍如何使用有限状态机来为一个简单的 Web 小部件 —— 一个能够淡入和淡出的工具提示 —— 设计复杂的行为。
本系列的第 2 部分将描述如何在 JavaScript 内实现此设计,以及如何充分利用 JavaScript 独特的语言特性,比如关联数组和函数闭包。第 3 部分则会涵盖如何使此实现能够在所有流行的 Web 浏览器中正常工作的内容。最终的代码紧凑简练,逻辑清晰透明,动画效果即使在负载极重的处理器上也能平稳流畅。
|
多年以来,Web 设计人员一直都通过在流行的 Web 浏览器内采用 JavaScript 解释器的方式来改善其网站的外观。他们的做法大都是将代码的简短片段复制到 HTML 页面中。当前,随着 Ajax 的日益流行,软件工程师也开始使用 JavaScript 来开发能在浏览器内执行的新一代的应用程序。基于浏览器的应用程序的规模不断扩大,这就相应要求采用其他执行环境成长和发展所使用的相同设计模式和开发原理。
|
基于浏览器的应用程序在实时环境中执行,在这种环境中鼠标、键盘、定时器、网络和程序事件都十分常见。当事件驱动的应用程序的行为取决于事件发生的顺序时,其编程就会变得非常复杂,也十分难以调试和修改。软件工程师早已开始使用 有限状态机 —— 学术领域有时又称其为离散或确定性有限自动机 —— 作为一种组织原理来开发事件驱动的程序了。
有限状态机通过用直观的表格代替复杂的逻辑为设计增加了严密性。从传统意义上讲,有限状态机对开发诸如网络驱动程序和编译器这类程序颇有帮助。有限状态机也同样有助于开发基于浏览器的应用程序。
在本系列中,您将练习开发一个样例有限状态机应用程序,来深入体验 JavaScript 语言的一些独特特性:
- 函数是一类 对象:与其它对象一样,函数可被创建,可赋给变量,也可作为参数传递。函数可在另一个函数内定义,还可赋给全局变量或作为结果返回。定义这些函数的函数返回之后,这些函数还会一直存在。
- 函数可以引用词法作用域 (包围函数定义的嵌套括号)内的任何变量,例如本地变量(由函数定义)。这些变量是函数闭包 的一部分(该函数、函数自身的变量和该函数所使用的在其词法作用域内定义的所有变量),而且在定义这些变量函数返回后,这些变量依然会存在。
- 函数可以存储于关联数组 中(关联数组是这样一类数组:它们按名称而不是数值索引)。
这些语言特性可以提供一种紧凑而简明的方式来为状态间的事件和转移组织动作,还可以提供一种巧妙的方式来兼容不同的浏览器事件模型。
样例应用程序 FadingTooltip 比内置于大多数浏览器的默认工具提示更为精致。用 FadingTooltip 小部件创建的工具提示使用动画式淡入和淡出代替突然弹出和消失,并可随光标移动。设计此行为所用的有限状态机模式使逻辑清晰透明。实现此行为所用的 JavaScript 语言特性则使源代码紧凑而有效。
本文展示了如何使用有限状态机的图、表表示设计一个动画式小部件的行为。本系列的后续文章会介绍如何在 JavaScript 内实现有限状态机的表表示以及如何处理与在流行的浏览器内进行测试和实现相关的实际问题。
当 光标暂时停留于一些可视控件 —— 比如按钮、选择器或输入字段 —— 时,时下的许多图形应用程序都能暂时显示包含相应的帮助性定义、操作说明或建议的小文本框。在早期的系统中,这些小文本框被称为 “气球帮助”,在 IBM 的一些产品中,称其为 infopop,在一些 Microsoft 产品中,其名字则是 ScreenTip。在本文,我使用的是其中更为常见的术语工具提示 。
现在一些流行的 Web 浏览器,比如 Netscape Navigator、Microsoft Internet Explorer、Opera 和 Mozilla Firefox,会为任何拥有 title
属性的 HTML 元素显示工具提示。例如,清单 1 中显示的这三个拥有 title
属性的 HTML 元素。
清单 1. 浏览器工具提示的 HTML 代码
|
样例页面 展示了浏览器如何呈现具有 title
属性的 HTML 元素。注意当光标在元素上移动时工具提示是如何出现和消失的。文本框包含简单的文本,这些文本无任何格式和样式。文本框会在光标短暂停留时弹出,并会在特 定时间过后、鼠标从此 HTML 元素移出或单击了某键的情况下突然消失。浏览器一次只显示一个文本框。工具提示的外观和行为已经硬性设定到浏览器内,无法更改。
内 置的工具提示还有很多可待提高之处,一些流行浏览器的最新版本为构建更为精致的工具提示提供了所需的 “原料”。HTML Division 元素创建了一个可在浏览器窗口的任何地方放置的提示框。通过级联样式表(CSS),您几乎可以设定框体外观的各个方面。用 JavaScript 编程实现的光标移动可以触发浏览器窗口内任意可视元素的特定动作。您还可以编制一个定时器来控制这些动作的顺序。
在 样例页面 可以找到具有这类工具提示的一些 HTML 元素。如果运行的是流行浏览器的最新版本,您就可以将更为精致的工具提示和内置的工具提示做一对比:
|
- 这类工具提示是淡入淡出的,而不是突然弹出和突然消失。
- 这类工具提示包含图像和文本,并经很好的格式化和样式化处理。
- 可见时,这类工具提示可以随光标移动。
- 当光标从 HTML 元素移出然后又移回此元素时,淡入淡出会反转方向。
- 同时可有多个工具提示可视,一些淡出,一些淡入。
这 些增强的行为和外观不仅有修饰的作用,还可以提高可用性。面对有数十个或数百个元素的繁忙页面,用户很可能会错过即刻弹出的工具提示。人类的视觉系统对运 动的物体十分敏感,因而也更容易注意到淡入视野并随鼠标而动的工具提示,即使用户的注意力不在这儿也没关系。对比未格式化过的文本,图像、格式化和样式化 能更有效地传递信息。而且,这些更为精致的工具提示的所有参数都是可配置的。
本文后面的内容将着重于介绍如何将 FadingTooltip 小部件设计为一个有限状态机。本系列的后续文章会为您展示如何实现和测试这些代码。如果您急于想知道这些代码,也可以在 参考资料 部分找到到相关 JavaScript 源代码和使用这些代码的一个 HTML Web 页面的链接。
|
|
有限状态机对行为建模,在该模型中,对将来事件的响应取决于先前的事件。此领域已出现了大量学术著作(参见 参考资料 ),而有限状态机的实用定义却十分简单明了。有限状态机就是包含如下内容的计算机程序:
- 事件 :程序对事件进行响应。
- 状态 :程序在事件间的状态。
- 转移 :对应于事件,状态间的转移。
- 动作 :转移过程中采取动作。
- 变量 :变量保存事件间的动作所需的值。
在行为由许多不同类型事件驱动以及对特定事件的响应取决于先前事件发生顺序的情况下,有限状态机最为有用。 驱动有限状态机的事件可以是计算机外部的(由键盘、鼠标、定时器或网络活动发起),也可以是计算机内部的(由本应用程序的其他部分或其他应用程序发起)。
状态是记起先前事件的一种方式,转移则用来组织对将来事件的响应。其中的一个状态必须要被指派为初始状态。结束状态可有可无,FadingTooltip 小部件就没有结束状态。
有限状态机的两种常见表示为:
-
方向图
-
气球状的圆圈代表状态,圆圈间的箭头线代表转移,它会被标以事件和动作。
- 表的行和列代表事件和状态,单元格内包含动作和转移。
用 有限状态机开发事件驱动程序比一般的过程式编程要复杂一些;一般来说,需要更多的规则,尤其是更多的设计精力。如果处理得当,有限状态机可以使代码简单、 测试迅速、维护轻松。但是,即便如此,有限状态机的复杂性使其并不能适合所有事件驱动的程序的开发。例如,当事件的种类不多或事件触发的动作总是相同时, 进行额外的开发可能会得不偿失。
|
|
有限状态机是事件驱动的,需要在它们的运行时环境将其与其相关的事件挂接起来。这可通过事件处理程序 实现,事件处理程序是一些可插入到运行时环境的小的代码片段,一旦特定事件发生,这些处理程序就会执行。事件处理程序执行时,需要获得如下一些基本信息:
- 已发生事件的类型(例如,光标移动、定时器超时)
- 事件的上下文(例如,光标位于哪个 HTML 元素之上、完成的是哪个网络请求)
- 有限状态机自身的变量和方法的位置
JavaScript 十分适合于构建事件驱动的有限状态机。事实上,JavaScript 有点太过适合 —— 它有三种挂接事件的方式。每种事件模型 都很直观明了,但程序必须实现所有三种模型以确保它们可以运行于所有流行的浏览器之上。事件的上下文在其中两个事件模型内被直接传递给事件处理程序;对于另外一个模型,JavaScript 函数闭包允许事件的上下文被包裹进其事件处理程序。
JavaScript 提供一种对象模型 ,对象模型是 Java 和 C++ 程序员所熟知的,它也可用来对有限状态机的变量和方法进行编码。而且,JavaScript 关联数组还允许直接对有限状态机的二维表进行编码。
|
|
有限状态机的基本要素是它所响应的事件及事件间的状态。设计必须考虑到每个可能状态的每个可能事件:
- 在该状态下,此事件是否可能发生
- 采取什么动作来处理事件
- 事件过后转移到什么状态
- 在事件之间需要记录什么变量
我以 图 1 所示的一个图形来开始设计的过程,图中气球形圆圈所示的是状态,连接这些圆圈的箭头线代表的是转移。最终获得的是一张表,如 图 4 所示,在该表的标题行和标题列分别列出了事件和状态。表中的一些单元格列出了当特定事件在特定状态发生时所要执行的动作,其它一些则表示在该状态下此事件不能发生。
通 常,需要反复执行此设计过程才能获得正确的图和表。对具有多个事件和状态的有限状态机,这个过程可能会十分乏味,每次重复都需要遵守一定的原则来系统地处 理表中的每一个单元格。这迫使您不得不考虑在每个可能的情况下您所想要的动作。您可能会发现还可以进一步完善这些行为,也可能会发现所需的状态较预计的要 多(或少),甚至会发现您必须重新整理单元格间的这些动作以正确定义每种情况下的行为。
这种设计有限状态机的系统过程虽然有些乏味但却十分值得。图 4 所示的完成后的表给出了此行动的所有逻辑,并可被直接转换为代码(参见 actionTransitionFunctions 源代码)。
|
|
要设计 FadingTooltip 小部件,您需要了解 JavaScript 的一些功能。在严谨设计的原则指导下,我只在这里给出基本的设计思想,而将具体的实现留待本系列后续文章中介绍。
当光标经过页面中的 HTML 元素时,所有流行的浏览器都能将事件传递给 JavaScript 代码。这些事件是 mouseover 、mousemove 和 mouseout ,分别代表光标已经移至、移上和移出 HTML 元素。浏览器用这些事件传递光标当前位置。当事件发生时,可用 JavaScript 编程动态创建 HTML Division 元素,用文本、图像和标记填充这些元素并将其定位到光标附近。
浏览器并没有原生的淡入和淡出函数,但可以通过改变 Division 元素的透明度(实际上是不透明度,透明度的反义词)来模拟这些函数。
JavaScript 有两类定时器:一次定时器在超时时生成 timeout 事件;重复断续器定期生成 timetick 事件。FadingTooltip 小部件需要这两种定时器。
|
|
首先回顾一下想要从 FadingTooltip 小部件获得的基本行为。当光标从特定的 HTML 元素上移过的时候,您可能想让此小部件等待光标在该元素上暂停。如果可以如此,之后您可能又想让此小部件将工具提示淡入,显示一会后再淡出。
有限状态机将需要响应以下事件:
- 当光标移至、移上和移出某一 HTML 元素时,浏览器能分别将 mouseover、mousemove 和 mouseout 事件传递给 JavaScript。
- JavaScript 可以编程实现 timeout 事件来指示光标已停止足够长的一段时间或工具提示已显示了足够长的一段时间,也可以编程实现 timetick 事件来分别增减工具提示淡入和淡出的不透明度。
您 将需要设计状态机在事件间等待的一些状态。需要调用小部件的初始状态 Inactive,小部件在该状态下等待被 mouseover 事件激活。小部件在 Pause 状态下等待直到 timeout 事件指示光标已经在 HTML 元素上停留了足够长的时间。之后在用 timetick 事件动画式淡入的同时,小部件会在 FadeIn 状态下等待,继而又会在 Display 状态等待另一个 timeout 事件。最后,在用更多 timetick 事件动画式淡出的同时,小部件会在 FadeOut 状态下等待。小部件转回到 Inactive 状态,在此状态下等待另一个 mouseover 事件。
图 1 是此过程相应的图形表示,其中的气球形圆圈代表状态,连接圆圈的箭头线代表转移,箭头线上的标注代表事件。双层边界的圆圈代表初始状态。
图 1. 状态图的初始设计
FadingTooltip 小部件必须针对它处理的每个事件采取动作:
- 当 mouseover 事件在 Inactive 状态发生时,在转入 Pause 状态等待之前,它必须要开启一个一次定时器。
- 当 timeout 事件发生时,在转入 FadeIn 状态等待之前,它必须要创建工具提示(初始不透明度值为零)并开启一个重复断续器。
- 每次发生 timetick 事件,它都要适当增加工具提示的不透明度。当达到工具提示的最大不透明度时,它必须在转入 Display 状态等待之前取消此重复断续器并开启另一个定时器。
- 当定时器的 timeout 事件发生时,它必须在转入 FadeOut 状态等待之前开启另一个重复断续器。
- 每次在 FadeOut 状态发生 timetick 事件时,它都必须要适当减少工具提示的不透明度。当工具提示的不透明度减少到零时,小部件会取消此重复断续器,删除工具提示并返回到 Inactive 状态,在该状态等待被另一个 mouseover 事件激活。
图 2 在触发这些动作的事件之下列出了这些动作。
图 2. 在初始状态图的事件下追加动作
|
|
上述的状态图是设计有限状态机的一个很好的开始。但表形式更适合于完成设计,原因是表可以给出事件和状态的所有组合以供参考。
要将状态图转换成状态表,可以在行标题内填上事件名,在列标题内填上状态名。这些名字的顺序是任意的;我在第一行的开始位置放入了初始状态,在第一列的开始位置放入了初始事件,随后将动作和每一事件的下一状态复制到表中适当的单元格内,如 图 3 所示。
图 3. 与初始状态图对应的初始状态表
|
|
要完成有限状态机的设计,需要顾及表中的每一个空单元格。您需要为每个单元格做这样的考虑:该事件是否可以发生在该状态,如果可以,小部件在这种情况下将采取什么动作,下一个状态又将是什么。这虽然有些乏味,但却是设计过程的必需部分。
考 虑单元格的顺序先后关系不大。通常在设计过程中需要多次重复此步骤,反复考虑每个单元格,不时地修改其内容,而且每次的考虑顺序都会有所不同。另外随着设 计的不断深入,添加(或删除)状态、做进一步的修改也十分常见。在这里,我将跳过这些反复过程,着重总结如何通过依次查看每个状态和事件来获得最终的结果 表。
-
Inactive 状态
-
在这种状态下,只有初始状态可以发生,原因是 mousemove 和 mouseout 事件应该继 mouseover 事件之后发生,而且没有任何定时器在运行。所以应将此列的所有其他单元格标记为“不应发生”。
在继续之前,还应注意一下此状态的 mouseover 事件。当为此工具提示创建 HTML Division 元素时,需要将它定位于光标的附近,所以要保存光标的当前位置,当前位置由浏览器与此事件一同传递。而且在开始新的定时器之前,最好能够取消任何运行着的定时器。在 mouseover 对应的单元格内添加上述动作。
Pause 状态
-
在 等待定时器超时时,光标可能会在 HTML 元素内移动或从此 HTML 元素移出。需要决定一旦发生这些事件所应采取的动作以及下一个状态是什么。如果在此状态发生 mouseout 事件,FadingTooltip 小部件应能返回 Inactive 状态,就像光标从未经过 HTML 元素一样,而且还必须取消定时器。在 mouseout 对应的单元格记录这些动作和转移。
另一方面,对于 mousemove 事件,则需要小部件能够继续等待光标悬停,这又要求取消和重新开启定时器。因为想要让工具提示出现在光标的附近,所以需要更新所保存的光标位置。 Pause 状态下的 mousemove 事件的动作和转移与 Inactive 状态下的 mousemove 事件的动作和转移相同。所以无需重复两个单元格的内容,在 mousemove 对应的单元格内放上同样的内容即可。将此列的所有其他单元格标记为“不应发生”。
FadeIn 状态
-
在 这种状态下,在用 timetick 事件处理淡入时,光标可以继续到处移动。如果发生 mousemove 事件,需相应移动工具提示并保持当前的状态不变。如果发生 mouseout 事件,转移到 FadeOut 状态,重复断续器仍会运行以便后续的 timetick 事件会在当前值的基础之上减少工具提示的不透明度。在适当的单元格内记录这些动作和转移并将此列的所有其他单元格标记为“不应发生”。
-
光 标仍可以到处移动。如果光标在 HTML 元素之内移动,采取与 FadeIn 状态相同的动作 —— 相应移动工具提示。如果光标从 HTML 元素移出,就采取与 Display 状态下的 timeout 事件相同的状态和转移。在 mousemove 和 mouseout 对应的单元格直接放上相同的内容并将此列的所有其他单元格标记为“不应发生”;
-
在 这种状态下,在用 timetick 事件处理淡出时,光标仍可继续到处移动。如果光标在 HTML 元素之内移动,采取与 FadeIn 和 Display 状态相同的动作。如果光标从 HTML 元素移出,不需要做任何事情 —— 重复断续器会继续运行以便后续的 timetick 事件会在当前值的基础之上减少工具提示的不透明度直到其值为零。
不要将此单元格标记为“不应发生”,而是应该标示为无需任何动作。如果光标又再次回到该 HTML 元素,将工具提示移回光标并返回 FadeIn 状态。
图 4 显示了所有这些动作和转移。剩下的空白单元格应标记为“不应发生”。
图 4. FadingTooltip 小部件设计后的状态表
有限状态机的状态表总是能转换回状态图,因为二者是等价的。图 5 显示了完整的状态表对应的状态图。
图 5. FadingTooltip 小部件设计后的状态图
|
|
完成状态表和状态图之后,很有必要对它再进行一次回顾来收集状态机在两事件间需要记录的变量以便状态机能够执行不同的单元格内的相应动作。有限状态机需要 清单 2 中所示的状态变量。
清单 2.初始的状态变量列表
|
虽然 JavaScript 变量本身不区分类型,但变量所包含的值是区分类型的(这就是说,任何类型的值都可以赋给变量)。根据这一原则,我列出了状态变量名并在注释部分给出了希望赋给这些变量的值的类型。
|
|
|