javascript 状态机

本系列的 第 1 部分 演示了如何用有限状态机系统化地为一个简单 Web 小部件(一个淡入和淡出视图的动画式工具提示)设计复杂行为。在本文中,您将学习如何用 JavaScript 实现这种行为,并充分利用语言的独特特性,包括关联数组和函数闭包。产生的代码既紧凑又简洁,它的逻辑是透明的,它的动画效果即使在负载很重的处理器上也能够平滑地执行。

第 3 部分将讨论如何让这个实现能够在所有流行浏览器上运行的实际问题。

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

第 1 部分 描述 Web 页面的一个工具提示部件,与流行的 Web 浏览器实现的内置实现相比,它具有更高级的行为。当鼠标光标停留在一个 HTML 元素上之后,这个 FadingTooltip 部件会淡入视图,工具提示显示一段时间之后就淡出视图。这个工具提示会跟随鼠标的移动,即使在淡入和淡出期间,而且当光标从 HTML 元素移出然后又移回此元素时,淡入淡出会反转方向。这种行为要求 FadingTooltip 部件能够响应各种不同的事件,而且在某些情况下,对特定事件的响应取决于以前发生的事件。

开发人员可以使用有限状态机设计模式来组织这样的事件驱动程序。在第 1 部分中,我们应用有限状态机的设计原理生成了一个定义所需行为的状态表,如图 1 所示。


图 1. FadingTooltip 部件的状态表
FadingTooltip 部件的状态表

状态表的行和列标上了部件要响应的事件的名称,以及在事件之间部件所处的状态。表中的每个单元格指定,在特定状态下发生特定事件时,部件将采取的操作。表单元格还可以指定采取操作之后部件将转移到的下一个状态,或者指定部件将保持同样的状态。空的表单元格表示在特定状态下不应该发生特定的事件。另外,在第 1 部分中,我们编写了一个 状态变量 列表,部件需要在事件之间记住这些变量,以便能够执行不同的单元格中的相关操作。

在第 3 部分中,将在流行的浏览器中测试这个实现,并处理某些不应该发生的情况。

第 1 部分指出 JavaScript 很适合作为有限状态机的执行环境,并提到它的一些与设计阶段相关的功能。在本文中,学习将设计转换为 JavaScript 的细节,利用一些优雅的语言特性,以及对一些不太优雅的细节进行调整以使实现更合理。

将设计转换为 JavaScript

在第 1 部分中完成了有限状态机的设计之后,就可以用 JavaScript 实现 FadingTooltip 部件了。这是从设计阶段到真实执行环境的转换阶段,也就是从轻松的抽象转换到实用性。

我们只考虑最流行的浏览器的最新版本:Netscape Navigator、Microsoft® Internet Explorer®、Opera 和 Mozilla Firefox。尽管这些执行环境的种类并不多,但是仍然会带来许多麻烦。我们必须处理将来自不同浏览器的鼠标和计时器事件连接到 JavaScript 程序的细节。有一种优雅的 JavaScript 语言特性称为函数闭包(function closure),它可以帮助您简化实现。还可以应用另一个优雅的 JavaScript 语言特性关联数组(associative array) 将状态表直接转换成代码。还会看到如何使用 HTML div 元素创建工具提示并指定样式,用文本和图像填充它,将它定位在鼠标旁边,使它淡入和淡出视图,并跟随鼠标的移动。

但是按照面向对象开发的精神,首先需要一个对象,它包含将实现的所有东西,所以我们首先开发这个对象。





回页首


一个包罗万象的对象

Web 设计人员常常将一些简短的 JavaScript 代码片段复制并粘贴到 HTML 页面中,FadingTooltip 部件的编程过程比这要复杂一些。软件工程师喜欢将部件的变量和方法分组在一个对象中,但是 JavaScript 对象模型在 Java™ 和 C++ 程序员看来可能有点儿奇怪。一个 JavaScript 对象就能够完全满足需要:它可以将变量和方法分组在一个对象中,然后为每个工具提示创建单独的数据实例。这些对象实例将共享同样的代码,并独立运行。

在 JavaScript 中,对象构造方法(constructor) 仅仅是一个函数 —— 这个函数的名称是对象的名称。这个部件需要知道它自己要连接到哪个 HTML 元素,以及要在工具提示中显示什么内容,所以要作为构造方法的参数指定这些,并将它们保存在对象中。(还需要有办法设置与工具提示的行为和外观相关的参数,所以也为此指定一个参数,并在本文后面使用它。)变量是无类型的,所以对象构造方法可能以清单 1 这样的代码开头。


清单 1. FadingTooltip 对象构造方法的 JavaScript 代码
				
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    this.htmlElement = htmlElement; // save pointer to HTML element whose mouse events 
                                    // are hooked to this object
    this.tooltipContent = tooltipContent; // save text and HTML tags for the tooltip's 
                                          // HTML Division element
    ...

在 JavaScript 中,可以在创建对象时或者在以后任何时候,给对象添加属性(property),属性可以是变量或方法;创建属性的办法是将一个值赋给它们,就像这个构造方法对 this.htmlElementthis.tooltipContent 属性所做的。

在 JavaScript 中,对象原型(prototype) 是一种用来创建对象的新实例的模板;它定义对象的初始属性及其初始值。我们首先在对象原型中定义第 1 部分中确定部件需要的状态变量,见清单 2。


清单 2. FadingTooltip 对象原型的 JavaScript 代码
				
FadingTooltip.prototype = { 
    currentState: null,    // current state of finite state machine (one of the state 
                           // names in the table below)
    currentTimer: null,    // returned by setTimeout, non-null if timer is running
    currentTicker: null,   // returned by setInterval, non-null if ticker is running
    currentOpacity: 0.0,   // current opacity of tooltip, between 0.0 and 1.0
    tooltipDivision: null, // pointer to HTML division element when tooltip is visible
    lastCursorX: 0,        // cursor x-position at most recent mouse event
    lastCursorY: 0,        // cursor y-position at most recent mouse event
    ...

对象原型是定义与有限状态机有关的几乎任何东西的合适位置:状态表、它的操作及其参数。还需要加上最后一点儿东西就可以完成对象构造方法 —— 连接鼠标事件,然后本文的其余部分将致力于填充对象原型。





回页首


连接鼠标事件

正如在第 1 部分中的 设计阶段 提到的,当鼠标进入和离开 HTML 元素以及在 HTML 元素内移动时,浏览器可以将事件传递给 JavaScript。这些事件包含有帮助的信息,比如事件类型和鼠标在页面上的当前位置。浏览器通过调用预先注册的函数来传递事件。不幸的是,注册这些函数以及将参数传递给它们的方式因浏览器而异。为了确保您的有限状态机可以连接到所有流行的浏览器中的鼠标事件,需要实现三个不同的事件模型。好在每个事件模型的代码都十分紧凑。不幸的是,代码的紧凑性掩盖了它的复杂性。

Mozilla Firefox、Opera 和 Netscape Navigator 的最新版本支持 World Wide Web Consortium(W3C)提议的 标准化事件模型(standardized event model)。这是首选的,因为很容易注册(和注销)事件函数,而且可以将浏览器处理的多个已注册函数链接起来。如果可用的话,可以调用 HTML 元素的 addEventListener 方法来连接鼠标事件,调用时要传递一个事件类型以及当 HTML 元素上发生此事件时调用的函数,如清单 3 所示。


清单 3. 连接鼠标事件的 JavaScript 代码
				
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    htmlElement.fadingTooltip = this;
    if (htmlElement.addEventListener) { // for FF and NS and Opera
        htmlElement.addEventListener(
            'mouseover', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mousemove', 
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
        htmlElement.addEventListener(
            'mouseout',  
            function(event) { this.fadingTooltip.handleEvent(event); }, 
            false);
    }
    ...

addEventListener 调用的第二个参数是匿名函数(anonymous function),也就是没有名称的函数。这是在 JavaScript 中在其他函数中定义函数的第一种方法,但不是惟一的方法,目前就采用这种方法。可以在 JavaScript 代码中的任何地方使用 function 关键字动态地定义匿名函数。它返回一个函数指针,可以像任何其他引用值一样使用它们。在 FadingTooltip 部件中,将函数指针作为参数传递给其他函数、测试它们是否为 null、将它们赋值给变量以及将它们声明为对象方法。

传递给 addEventListener 方法的匿名函数看起来并不复杂。当鼠标事件发生时,浏览器将调用它们,将 event 对象传递给它们,它们将传递给 FadingTooltip 对象的 handleEvent 方法。浏览器的事件对象包含事件类型以及鼠标位置,所以一个 handleEvent 方法可以处理部件必须响应的所有鼠标事件。

这些简单的匿名函数还执行另一个重要而微妙的任务。在 W3C 事件模型中,用 HTML 元素的 addEventListener 方法注册的函数会成为这个元素的方法,所以当浏览器调用它们时,内置的 this 变量会指向这个 HTML 元素。但是,handleEvent 方法需要一个包含状态变量的 FadingTooltip 对象的指针。一种实现方式是在 HTML 元素上添加一个 fadingTooltip 属性,这个属性指向 FadingTooltip 对象,然后用它调用对象的 handleEvent 方法。这样的话,当执行 handleEvent 方法时,this 会指向 FadingTooltip 对象。

在 Internet Explorer 中连接鼠标事件

Microsoft Internet Explorer 当前不支持提议的 W3C 标准事件模型,而是提供它自己的一个相似的事件模型。它们之间的差异如下:

  • 事件类型略有不同
  • 注册的函数不会成为 HTML 元素的方法
  • 事件对象留在全局的 window 对象中
如果可用的话,可以调用 HTML 元素的 attachEvent 方法来连接事件,调用时要传递略有不同的事件类型和函数,见 清单 4

这是使用函数闭包在函数定义中封闭变量的第一种方法,但不是惟一的方法,目前就采用这种方法。


清单 4. 在 Internet Explorer 中连接鼠标事件的 JavaScript 代码
				
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else if (htmlElement.attachEvent) { // for MSIE 
        htmlElement.attachEvent(
            'onmouseover', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmousemove', 
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
        htmlElement.attachEvent(
            'onmouseout',  
            function() { htmlElement.fadingTooltip.handleEvent(window.event); } );
    } 
    ...

用 HTML 元素的 attachEvent 方法注册的函数不会成为这个元素的方法。当鼠标事件发生时,浏览器会调用它们,但是内置的 this 变量将指向全局的 window 对象,而不是 HTML 元素,所以函数不能通过 HTML 元素中保存的指针找到它们的 FadingTooltip 对象。

幸运的是,匿名函数定义位于对象构造方法的 htmlElement 参数的词法范围内。只需在匿名函数定义中使用 htmlElement 变量,就可以用这些函数封闭它。这称为函数闭包(function closure):当在另一个函数内定义一个函数时,如果内部函数使用外部函数的局部变量,JavaScript 就会用内部函数的定义保存这些变量。这样的话,当外部函数返回之后,在调用内部函数时,外部函数的局部变量仍然是可用的。

在这里,当构造方法返回之后,JavaScript 仍然保留 htmlElement 变量的值,所以当浏览器调用匿名函数时,匿名函数仍然可以使用这个变量。这使它们能够找到它们的 HTML 元素并通过指针引用它们的 FadingTooltip 对象,而不需要浏览器的帮助。

因为函数闭包是 JavaScript 语言的一项特性,所以它们在使用 W3C 事件模型的浏览器中一样是有效的。可以利用这个特性将构造方法的 htmlElement 参数值封闭在前一节定义的匿名函数中,而不使用内置的 this 变量。

在老式浏览器中连接鼠标事件

对于既不支持 W3C 事件模型,也不支持 Internet Explorer 事件模型的老式浏览器,必须使用 Netscape Navigator 早期版本提供的原始事件模型来连接事件。所有流行的浏览器都支持它,而且 Web 设计人员广泛使用它在 Web 页面上建立动画;但是对于实现更复杂的应用程序,这是最后的选择,因为它不能链接多个事件处理器。为此,需要将以前注册的事件函数的指针封闭在自己的事件函数定义中,然后在调用自己的 handleEvent 方法之后调用它们,见清单 5。


清单 5. 在老式浏览器中连接鼠标事件的 JavaScript 代码
				
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    else { // for older browsers
        var self = this;
        var previousOnmouseover = htmlElement.onmouseover;
        htmlElement.onmouseover = function(event) { 
            self.handleEvent(event ? event : window.event); 
            if (previousOnmouseover) { 
                htmlElement.previousHandler = previousOnmouseover;
                htmlElement.previousHandler(event ? event : window.event); 
            }
        };
        ... and similarly for 'onmousemove' and 'onmouseout' ...
    }
}

但是注意,这种方法是不完整的。它允许部件注册其他部件已经注册的相同事件,然后将它们链接在一起,但是不允许注销其他事件函数,因为链接的指针对于它们不可访问。

为了适应性更强,代码将构造方法的 this 变量(它指向 FadingTooltip 对象)复制到局部变量 self 中,然后使用 self 指针在匿名函数定义中定位 FadingTooltip 对象。这就把 FadingTooltip 对象的指针封闭在匿名函数定义中,所以当任何浏览器调用它们时,它们都可以直接定位 FadingTooltip 对象,而不依靠浏览器提供 HTML 元素的指针,也不需要将 FadingTooltip 对象的指针存储在 HTML 元素中。

对于为 W3C 和 Microsoft 事件模型定义的匿名函数,都可以将 FadingTooltip 对象的指针封闭在其中。这样就不必将对象的指针保存在 HTML 元素中,并可以在所有事件模型中应用同样的 HTML 元素定位技术。源代码 中的构造方法就采用这种方法。

既然已经连接了所有流行的浏览器中的鼠标事件,对象构造方法已经完整了,可以返回到对象原型了。





回页首


设置计时器并连接计时器事件

我们已经完成了 FadingTooltip 构造方法,可以继续填充它的原型。在 JavaScript 中,对象原型可以包含方法和变量;方法仅仅是指向函数的变量。首先定义一些通用的方法,它们启动和取消计时器。

在第 1 部分中的设计阶段提到过,JavaScript 提供两种类型的计时器:一次定时器和重复断续器,有限状态机需要这两种定时器。可以通过调用 setTimeoutsetInterval 函数启动计时器,传递的参数是一个时间值(以毫秒为单位)以及当发生 timeout 或 timetick 事件时要调用的函数。它们返回不透明度的引用,可以将这些引用传递给 clearTimeoutclearInterval 函数来取消计时器。

当超过 timeout 值指定的时间时,或者在每次到达 timetick 时间间隔时,浏览器将调用传递给 setTimeoutsetInterval 函数的计时器事件函数(对于 timetick,这个过程一直重复到取消计时器为止)。但是,这些 timeouttimetick 函数不会成为任何对象的方法。当浏览器调用它们时,this 变量指向全局的 window 对象。浏览器并不将关于计时器事件的任何信息传递给这些函数。

学会处理 鼠标事件 之后,连接计时器事件也就不困难了。当设置计时器时,将内置的 this 变量(它指向包含状态变量的 FadingTooltip 对象)复制到局部变量 self 中。self 变量处于 setTimeoutsetInterval 函数调用的词法范围。然后,定义使用 self 变量的匿名函数,并将它们作为参数传递给 setTimeoutsetInterval 函数。这将 self 变量封闭在函数定义中,所以当浏览器调用函数时它仍然可用,见清单 6。


清单 6. 设置计时器并连接计时器事件的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    startTimer: function(timeout) { 
        var self = this;
        this.currentTimer = 
            setTimeout( function() { self.handleEvent( { type: 'timeout' } ); }, 
            timeout);
    },
    startTicker: function(interval) { 
        var self = this;
        this.currentTicker = 
            setInterval( function() { self.handleEvent( { type: 'timetick' } ); }, 
            interval);
    },
    ...

计时器事件函数没有鼠标事件函数那么复杂。它们仅仅创建一个简单的计时器事件对象,其中只包含一种事件类型 —— timeout 或者 timetick,并将它传递给处理鼠标事件的同一个 handleEvent 方法。





回页首


创建操作/转换表

在 JavaScript 中,对象原型可以包含数组等数据结构和其他对象,以及变量和方法。普通数组的元素用整数作为索引,而关联数组的元素用名称作为索引,而不是整数。在 JavaScript 中,关联数组和对象仅仅是用来访问相同数据的不同语法:可以以关联数组元素的形式访问对象属性,见清单 7。


清单 7. 以关联数组元素的形式访问对象属性的 JavaScript 代码
				
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true

因此我们将 状态表 实现为一个二维的函数关联数组。直接使用状态名称和事件名称作为索引。数组的非空单元格指向匿名函数,这些匿名函数通过调用实用程序方法(比如启动和取消 计时器 的函数)来为事件执行操作,然后返回下一个状态。handleEvent 方法的代码将使用数组语法调用这些操作/转换函数,如清单 8 中的代码所示。


清单 8. 调用关联数组中存储的匿名函数的 JavaScript 代码
				
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event);

handleEvent 方法以关联数组的形式访问 actionTransitionFunctions 表,使用当前状态和事件类型作为索引,并选择要调用的函数。它将事件对象作为参数传递给这个函数。这个函数将执行所需的操作,然后返回下一个状态的名称。

因为关联数组是对象(反之亦然),所以可以使用对象语法定义 actionTransitionFunctions 表,但是 handleEvent 方法将使用数组语法访问它。例如,在 Inactive 的初始状态中,可能出现的惟一事件是 mouseover,所以可以定义一个处理此情况的函数,见清单 9。


清单 9. 将匿名函数存储为对象属性的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    initialState: 'Inactive',
    actionTransitionFunctions: { 
        Inactive: {
            mouseover: function(event) { 
                this.cancelTimer();
                this.saveCursorPosition(event);
                this.startTimer(this.pauseTime*1000);
                return 'Pause';
            }
        },
        ...

FadingTooltip 对象的原型包含 actionTransitionFunctions 属性,其值是另一个对象。它包含另一个属性 Inactive,其值也是另一个对象。它只包含一个属性 mouseover,其值是一个函数。当在 Inactive 状态下发生 mouseover 事件时,handleEvent 方法将调用这个函数。它需要一个名为 event 的参数,通过调用三个实用程序函数来执行三个操作,然后返回 Pause 作为下一个状态的名称。操作包括保存鼠标位置(这是浏览器存储在鼠标事件对象中的)和启动计时器,其超时值是一个名为 pauseTime 的参数(以秒作为单位,所以按照 startTimer 方法的要求,将它转换为毫秒)。

部件在 Pause 状态下需要响应三个事件:mousemovemouseouttimeout 事件。在 actionTransitionFunctions 表中定义一个 Pause 对象,它具有分别对应于这些事件类型的属性,如清单 10 所示。


清单 10. 在 Pause 状态下响应鼠标事件的函数的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        Pause: {
           mousemove: function(event) { 
              return this.doActionTransition('Inactive', 'mouseover', event);
           },
           mouseout: function(event) { 
              this.cancelTimer();
              return 'Inactive';
           },
           timeout: function(event) { 
              this.cancelTimer();
              this.createTooltip();
              this.startTicker(1000/this.fadeRate);
              return 'FadeIn';
           }
        },
        ...

当在 Pause 状态下发生 mousemove 事件时,handleEvent 方法将调用一个函数,这个函数简单地调用 doActionTransition 方法,传递 event 参数,并返回它所返回的值。与 handleEvent 方法相似,doActionTransition 方法使用它的前两个参数作为数组索引访问 actionTransitionFunctions 表,并将它的第三个参数传递给在数组中找到的函数。当发生 mouseout 事件时,代码调用一个函数,它会取消本节前面启动的计时器,然后转换回 Inactive 状态。

当发生 timeout 事件时,将取消任何正在运行的计时器,创建一个初始不透明度为 0 的工具提示,启动一个断续器,并转换到 FadeIn 状态。

actionTransitionFunctions 表中的其他函数一样,定义一个在 FadeIn 状态下处理 timetick 事件的函数,见清单 11。


清单 11. 在 FadeIn 状态下响应计时器事件的函数的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    actionTransitionFunctions: { 
        ...
        FadeIn: {
            ...
            timetick: function(event) {
                this.fadeTooltip(+this.tooltipOpacity/(this.fadeinTime*this.fadeRate));
                if (this.currentOpacity>=this.tooltipOpacity) {
                    this.cancelTicker();
                    this.startTimer(this.displayTime*1000);
                    return 'Display';
                }
                return this.CurrentState;
            }
        },
        ....

每当在 FadeIn 状态下发生 timetick 事件时,handleEvent 方法将调用一个函数,它略微增加工具提示的不透明度。淡入时间(以秒为单位指定)、动画速率(不透明度从 0 开始增加的速度,用每秒的步数指定)和最大不透明度(指定为 0.0 到 1.0 之间的浮点数)都是参数。这个函数将返回当前状态,让有限状态机保持在 FadeIn 状态,直到工具提示的不透明度到达最大不透明度参数。然后,它取消断续器,启动一个计时器来显示工具提示,并转换到 Display 状态。

以相似的方式定义 actionTransitionFunctions 表中的其他函数。细节请参考完整的 源代码(其中有很多注释),并参照 图 1





回页首


实现事件处理器

我们已经多次提到 handleEvent 方法,所以它的实现应该并不神秘了,见清单 12。


清单 12. 事件处理器的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    handleEvent: function(event) { 
        var actionTransitionFunction = 
            this.actionTransitionFunctions[this.currentState][event.type];
        if (!actionTransitionFunction) 
            actionTransitionFunction = this.unexpectedEvent;
        var nextState = actionTransitionFunction.call(this, event);
        if (!this.actionTransitionFunctions[nextState]) 
            nextState = this.undefinedState(nextState);
        this.currentState = nextState;
    },
    ... 

访问 actionTransitionFunctions 表的实际实现与 前一节 中的建议不太一样。这个方法使用当前状态和事件类型作为关联数组的索引,从 actionTransitionFunctions 表中选择要调用的函数。但是,这个方法将所选函数的指针复制到一个局部变量中,然后用 function 对象的 call 方法调用这个函数。而不是直接调用它。能够这样做是因为,与其他值一样,function 对象可以赋值给变量。必须这样做是因为,当执行函数时,内置的 this 变量需要指向 FadingTooltip 对象。如果像前面建议的那样,使用数组索引从 actionTransitionFunctions 表直接调用函数,this 变量就会指向这个表。function 对象的 call 方法会将 this 设置为它的第一个参数,然后调用函数,传递其余的参数。

请记住,actionTransitionFunctions 表是稀疏的;为每个状态下期望出现的事件定义函数,其他单元格都空着。handleEvent 方法通过调用 unexpectedEvent 方法来处理任何不期望出现的事件。如果某个操作/转换函数返回不属于有效状态的值,它将调用 undefinedState 方法。这些方法将取消任何正在运行的计时器,如果已经创建了工具提示,就删除它,并将有限状态机返回到初始状态。一个方法见清单 13;另一个方法几乎是相同的。


清单 13. 不期望出现的事件的处理器的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    unexpectedEvent: function(event) { 
        this.cancelTimer();
        this.cancelTicker();
        this.deleteTooltip();
        alert('FadingTooltip received unexpected event ' + event.type + 
              ' in state ' + this.currentState);
        return this.initialState; 
    },	
    ... 

这些方法将显示一个描述错误的警告对话框,希望用户将错误描述发给代码的作者。





回页首


最终显示工具提示

除了工具提示本身之外,所有东西都实现了,现在不用再等了。

当在 Pause 状态下出现 timeout 事件时,希望工具提示出现在鼠标光标附近,但是浏览器没有将鼠标位置传递给计时器事件。幸运的是,浏览器会将鼠标位置传递给鼠标事件,所以当发生鼠标事件时,可以调用 saveCursorPosition 方法将它保存在状态变量中,见清单 14。


清单 14. 保存鼠标位置的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    saveCursorPosition: function(event) {
        this.lastCursorX = event.clientX;
        this.lastCursorY = event.clientY;
    },
    ... 

工具提示是一个 HTML div 元素,其中可以包含任何文本、图像和标记,它在 tooltipContent 参数中传递给构造方法。createTooltip 方法见清单 15。


清单 15. 创建工具提示的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    createTooltip: function() {     
        this.tooltipDivision = document.createElement('div');
        this.tooltipDivision.innerHTML = this.tooltipContent;
        
        if (this.tooltipClass) {
            this.tooltipDivision.className = this.tooltipClass;
        } else {
            this.tooltipDivision.style.minWidth = '25px';
            this.tooltipDivision.style.maxWidth = '350px';
            this.tooltipDivision.style.height = 'auto';
            this.tooltipDivision.style.border = 'thin solid black';
            this.tooltipDivision.style.padding = '5px';
            this.tooltipDivision.style.backgroundColor = 'yellow';
        }
        
        this.tooltipDivision.style.position = 'absolute';
        this.tooltipDivision.style.zIndex = 101;
        this.tooltipDivision.style.left = this.lastCursorX + this.tooltipOffsetX;
        this.tooltipDivision.style.top = this.lastCursorY + this.tooltipOffsetY;
        
        this.currentOpacity = this.tooltipDivision.style.opacity = 0;
        
        document.body.appendChild(this.tooltipDivision);                
    },	
    ... 

如果在参数中指定了 CSS 类名,就应用它控制 HTML div 元素的外观。否则,就应用默认的基本样式。但是工具提示的几个方面依赖于它的外观,比如它的位置和不透明度,所以要覆盖与这些属性相关的任何样式,这可以在样式表中指定。HTML div 元素将用绝对坐标定位在页面上,接近最近保存的鼠标位置,在任何重叠的其他元素上面。它的初始不透明度是 0,即完全透明。

每当在 FadeInFadeOut 状态下发生 timetick 事件时,分别调用 fadeTooltip 方法略微增加或减少工具提示的不透明度,同时确保不透明度处于 0 和最大不透明度参数之间,见清单 16。


清单 16. 淡入/淡出工具提示的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    fadeTooltip: function(opacityDelta) { 
        this.currentOpacity += opacityDelta;
        if (this.currentOpacity<0) 
            this.currentOpacity = 0;
        if (this.currentOpacity>this.tooltipOpacity) 
            this.currentOpacity = this.tooltipOpacity;
        this.tooltipDivision.style.opacity = this.currentOpacity;
    },	
    ... 

操作/转换函数也需要移动和删除工具提示的实用程序方法。它们的实现非常简单明了,可以通过 源代码文件 中的注释理解它们。

正如在本文的这一部分中提到的,需要定义参数才能完成实现。它们是对象原型的属性,但是与状态变量不同,它们具有清单 17 所示的默认值。


清单 17. 在对象原型中定义参数的 JavaScript 代码
				
FadingTooltip.prototype = { 
    ...
    tooltipClass: null,  // name of a CSS style to apply to the tooltip, or 
                         // 'null' for default style
    tooltipOpacity: 0.8, // maximum opacity of tooltip, between 0.0 and 1.0 
                         // (after fade-in, before fade-out)
    tooltipOffsetX: 10,  // horizontal offset from cursor to upper-left 
                         // corner of tooltip
    tooltipOffsetY: 10,  // vertical offset from cursor to upper-left 
                         // corner of tooltip
    fadeRate: 24,        // animation rate for fade-in and fade-out, in 
                         // steps per second
    pauseTime: 0.5,      // how long the cursor must pause over HTML 
                         // element before fade-in starts, in seconds
    displayTime: 10,     // how long to display tooltip (after fade-in, 
                         // before fade-out), in seconds
    fadeinTime: 1,       // how long fade-in animation will take, in seconds
    fadeoutTime: 3,      // how long fade-out animation will take, in seconds	
    ... 
};

对象构造方法的可选参数 parameters 是一个用 JavaScript Object Notation(有时称为 JSON)编写的对象,它可以覆盖这些属性的默认值,见清单 18。


清单 18. 在对象构造方法中进行参数初始化的 JavaScript 代码
				
function FadingTooltip(htmlElement, tooltipContent, parameters) { 
    ...
    for (parameter in parameters) { 
        if (typeof(this[parameter])!='undefined') 
            this[parameter] = parameters[parameter];
    }
    ...
};

构造方法在它的 parameters 参数中检查每个属性;对于每个属性,如果它存在于原型中,那么它的值覆盖参数的默认值。请记住,原型是一个对象,所以它也是一个关联数组。这里同样使用对象表示法定义参数,但是用数组表示法访问它们。

现在,FadingTooltip 的实现已经完成了。您可以 下载 构造方法和原型的源代码。





回页首


关于性能的几点说明

在对实现进行测试之前,要对性能做几点说明。

浏览器同步地执行 JavaScript 程序。当连接的事件发生时,浏览器调用它的事件处理器,并等待它返回,然后再继续处理下一个事件。如果在事件处理器返回之前发生了更多的事件,浏览器就将它们放在队列中;当事件处理器返回时,依次同步地处理排队的事件,每次一个。如果一个事件处理器花费了过长时间,它可能会延迟浏览器本身对未连接的事件的响应。用户就可能认为程序反应缓慢,或者认为浏览器出了故障。

所以一定要使事件处理器尽可能简短,这在用密集的计时器事件模拟动画的程序中尤其重要。如果 timetick 事件处理器花费的时间超过了断续器的时间间隔,timetick 事件就会在浏览器的事件队列中积累起来,导致处理器饱和并使浏览器反应缓慢。

例如,假设动画的默认速率是每秒 24 步,一个 timetick 事件处理器在返回到浏览器之前,有差不多 40 毫秒时间完成它需要的操作(假设它占用全部处理器时间)。在现代的工作站上,这段时间足够进行许多处理。但是,我们的目标不是在这段时间内做尽可能多的工作,而是使用尽可能少的处理器时间。如果程序实现的处理器使用率非常低,那么即使在其他活动的负载很高的处理器上,动画效果也会平滑地运行,程序能够做出正常的响应。

不要将动画速率设置为每秒 60 或 85 步(因为您认为动画速率与显示器的刷新频率匹配会产生更平滑的动画)。这会将 timetick 事件之间的时间减少到大约 12 毫秒。如果 timetick 事件处理器花费的时间超过这个值,或者有其他活动争夺处理器,那么动画可能变得不平滑,或者浏览器变得响应缓慢。

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值