有限狀態(tài)機很早就已用作設(shè)計和實現(xiàn)事件驅(qū)動的程序(比如網(wǎng)絡(luò)適配器和編譯器)內(nèi)復(fù)雜行為的組織原則?,F(xiàn)在,可編程的 Web 瀏覽器為新一代的應(yīng)用程序開辟了一種全新的事件驅(qū)動環(huán)境,?;跒g覽器的應(yīng)用程序因 Ajax 而廣為流行,而同時也變得更為復(fù)雜,。程序設(shè)計人員和實現(xiàn)人員能夠大大受益于有限狀態(tài)機的原理和結(jié)構(gòu),。
第 1 部分 描述 Web 頁面的一個工具提示部件,與流行的 Web 瀏覽器實現(xiàn)的內(nèi)置實現(xiàn)相比,,它具有更高級的行為,。當鼠標光標停留在一個 HTML 元素上之后,,這個 FadingTooltip 部件會淡入視圖,,工具提示顯示一段時間之后就淡出視圖。這個工具提示會跟隨鼠標的移動,,即使在淡入和淡出期間,而且當光標從 HTML 元素移出然后又移回此元素時,,淡入淡出會反轉(zhuǎn)方向,。這種行為要求 FadingTooltip 部件能夠響應(yīng)各種不同的事件,而且在某些情況下,,對特定事件的響應(yīng)取決于以前發(fā)生的事件,。
開發(fā)人員可以使用有限狀態(tài)機設(shè)計模式來組織這樣的事件驅(qū)動程序。在第 1 部分中,,我們應(yīng)用有限狀態(tài)機的設(shè)計原理生成了一個定義所需行為的狀態(tài)表,如圖 1 所示,。
圖 1. FadingTooltip 部件的狀態(tài)表
狀態(tài)表的行和列標上了部件要響應(yīng)的事件的名稱,,以及在事件之間部件所處的狀態(tài),。表中的每個單元格指定,在特定狀態(tài)下發(fā)生特定事件時,,部件將采取的操作。表單元格還可以指定采取操作之后部件將轉(zhuǎn)移到的下一個狀態(tài),,或者指定部件將保持同樣的狀態(tài),??盏谋韱卧癖硎驹谔囟顟B(tài)下不應(yīng)該發(fā)生特定的事件。另外,,在第 1 部分中,我們編寫了一個 狀態(tài)變量 列表,,部件需要在事件之間記住這些變量,以便能夠執(zhí)行不同的單元格中的相關(guān)操作,。
|
在第 3 部分中,,將在流行的瀏覽器中測試這個實現(xiàn),,并處理某些不應(yīng)該發(fā)生的情況。 |
|
第 1 部分指出 JavaScript 很適合作為有限狀態(tài)機的執(zhí)行環(huán)境,,并提到它的一些與設(shè)計階段相關(guān)的功能,。在本文中,學(xué)習(xí)將設(shè)計轉(zhuǎn)換為 JavaScript 的細節(jié),,利用一些優(yōu)雅的語言特性,以及對一些不太優(yōu)雅的細節(jié)進行調(diào)整以使實現(xiàn)更合理,。
將設(shè)計轉(zhuǎn)換為 JavaScript
在第 1 部分中完成了有限狀態(tài)機的設(shè)計之后,就可以用 JavaScript 實現(xiàn) FadingTooltip 部件了,。這是從設(shè)計階段到真實執(zhí)行環(huán)境的轉(zhuǎn)換階段,,也就是從輕松的抽象轉(zhuǎn)換到實用性,。
我們只考慮最流行的瀏覽器的最新版本:Netscape Navigator,、Microsoft® Internet Explorer®、Opera 和 Mozilla Firefox,。盡管這些執(zhí)行環(huán)境的種類并不多,,但是仍然會帶來許多麻煩。我們必須處理將來自不同瀏覽器的鼠標和計時器事件連接到 JavaScript 程序的細節(jié),。有一種優(yōu)雅的 JavaScript 語言特性稱為函數(shù)閉包(function closure),它可以幫助您簡化實現(xiàn),。還可以應(yīng)用另一個優(yōu)雅的 JavaScript 語言特性關(guān)聯(lián)數(shù)組(associative array) 將狀態(tài)表直接轉(zhuǎn)換成代碼。還會看到如何使用 HTML div 元素創(chuàng)建工具提示并指定樣式,,用文本和圖像填充它,將它定位在鼠標旁邊,,使它淡入和淡出視圖,并跟隨鼠標的移動,。
但是按照面向?qū)ο箝_發(fā)的精神,首先需要一個對象,,它包含將實現(xiàn)的所有東西,所以我們首先開發(fā)這個對象,。
一個包羅萬象的對象
Web 設(shè)計人員常常將一些簡短的 JavaScript 代碼片段復(fù)制并粘貼到 HTML 頁面中,F(xiàn)adingTooltip 部件的編程過程比這要復(fù)雜一些,。軟件工程師喜歡將部件的變量和方法分組在一個對象中,,但是 JavaScript 對象模型在 Java™ 和 C++ 程序員看來可能有點兒奇怪。一個 JavaScript 對象就能夠完全滿足需要:它可以將變量和方法分組在一個對象中,,然后為每個工具提示創(chuàng)建單獨的數(shù)據(jù)實例,。這些對象實例將共享同樣的代碼,并獨立運行,。
在 JavaScript 中,對象構(gòu)造方法(constructor) 僅僅是一個函數(shù) —— 這個函數(shù)的名稱是對象的名稱,。這個部件需要知道它自己要連接到哪個 HTML 元素,,以及要在工具提示中顯示什么內(nèi)容,,所以要作為構(gòu)造方法的參數(shù)指定這些,,并將它們保存在對象中,。(還需要有辦法設(shè)置與工具提示的行為和外觀相關(guān)的參數(shù),所以也為此指定一個參數(shù),,并在本文后面使用它。)變量是無類型的,,所以對象構(gòu)造方法可能以清單 1 這樣的代碼開頭。
清單 1. FadingTooltip 對象構(gòu)造方法的 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 中,,可以在創(chuàng)建對象時或者在以后任何時候,給對象添加屬性(property),,屬性可以是變量或方法,;創(chuàng)建屬性的辦法是將一個值賦給它們,就像這個構(gòu)造方法對 this.htmlElement 和 this.tooltipContent 屬性所做的,。
在 JavaScript 中,,對象原型(prototype) 是一種用來創(chuàng)建對象的新實例的模板;它定義對象的初始屬性及其初始值,。我們首先在對象原型中定義第 1 部分中確定部件需要的狀態(tài)變量,,見清單 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
...
|
對象原型是定義與有限狀態(tài)機有關(guān)的幾乎任何東西的合適位置:狀態(tài)表,、它的操作及其參數(shù)。還需要加上最后一點兒東西就可以完成對象構(gòu)造方法 —— 連接鼠標事件,,然后本文的其余部分將致力于填充對象原型。
連接鼠標事件
正如在第 1 部分中的 設(shè)計階段 提到的,,當鼠標進入和離開 HTML 元素以及在 HTML 元素內(nèi)移動時,瀏覽器可以將事件傳遞給 JavaScript,。這些事件包含有幫助的信息,,比如事件類型和鼠標在頁面上的當前位置,。瀏覽器通過調(diào)用預(yù)先注冊的函數(shù)來傳遞事件,。不幸的是,注冊這些函數(shù)以及將參數(shù)傳遞給它們的方式因瀏覽器而異,。為了確保您的有限狀態(tài)機可以連接到所有流行的瀏覽器中的鼠標事件,需要實現(xiàn)三個不同的事件模型,。好在每個事件模型的代碼都十分緊湊。不幸的是,,代碼的緊湊性掩蓋了它的復(fù)雜性。
Mozilla Firefox,、Opera 和 Netscape Navigator 的最新版本支持 World Wide Web Consortium(W3C)提議的 標準化事件模型(standardized event model),。這是首選的,因為很容易注冊(和注銷)事件函數(shù),,而且可以將瀏覽器處理的多個已注冊函數(shù)鏈接起來。如果可用的話,可以調(diào)用 HTML 元素的 addEventListener 方法來連接鼠標事件,,調(diào)用時要傳遞一個事件類型以及當 HTML 元素上發(fā)生此事件時調(diào)用的函數(shù),如清單 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 調(diào)用的第二個參數(shù)是匿名函數(shù)(anonymous function),也就是沒有名稱的函數(shù),。這是在 JavaScript 中在其他函數(shù)中定義函數(shù)的第一種方法,,但不是惟一的方法,,目前就采用這種方法??梢栽?JavaScript 代碼中的任何地方使用 function 關(guān)鍵字動態(tài)地定義匿名函數(shù)。它返回一個函數(shù)指針,,可以像任何其他引用值一樣使用它們,。在 FadingTooltip 部件中,將函數(shù)指針作為參數(shù)傳遞給其他函數(shù),、測試它們是否為 null ,、將它們賦值給變量以及將它們聲明為對象方法。
傳遞給 addEventListener 方法的匿名函數(shù)看起來并不復(fù)雜,。當鼠標事件發(fā)生時,瀏覽器將調(diào)用它們,,將 event 對象傳遞給它們,,它們將傳遞給 FadingTooltip 對象的 handleEvent 方法,。瀏覽器的事件對象包含事件類型以及鼠標位置,所以一個 handleEvent 方法可以處理部件必須響應(yīng)的所有鼠標事件,。
這些簡單的匿名函數(shù)還執(zhí)行另一個重要而微妙的任務(wù)。在 W3C 事件模型中,,用 HTML 元素的 addEventListener 方法注冊的函數(shù)會成為這個元素的方法,所以當瀏覽器調(diào)用它們時,,內(nèi)置的 this 變量會指向這個 HTML 元素,。但是,handleEvent 方法需要一個包含狀態(tài)變量的 FadingTooltip 對象的指針,。一種實現(xiàn)方式是在 HTML 元素上添加一個 fadingTooltip 屬性,,這個屬性指向 FadingTooltip 對象,,然后用它調(diào)用對象的 handleEvent 方法,。這樣的話,,當執(zhí)行 handleEvent 方法時,this 會指向 FadingTooltip 對象,。
在 Internet Explorer 中連接鼠標事件
Microsoft Internet Explorer 當前不支持提議的 W3C 標準事件模型,而是提供它自己的一個相似的事件模型,。它們之間的差異如下:
- 事件類型略有不同
- 注冊的函數(shù)不會成為 HTML 元素的方法
- 事件對象留在全局的 window 對象中
如果可用的話,,可以調(diào)用 HTML 元素的 attachEvent 方法來連接事件,調(diào)用時要傳遞略有不同的事件類型和函數(shù),,見 清單 4。
這是使用函數(shù)閉包在函數(shù)定義中封閉變量的第一種方法,,但不是惟一的方法,,目前就采用這種方法,。
清單 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 方法注冊的函數(shù)不會成為這個元素的方法,。當鼠標事件發(fā)生時,,瀏覽器會調(diào)用它們,但是內(nèi)置的 this 變量將指向全局的 window 對象,,而不是 HTML 元素,,所以函數(shù)不能通過 HTML 元素中保存的指針找到它們的 FadingTooltip 對象,。
幸運的是,,匿名函數(shù)定義位于對象構(gòu)造方法的 htmlElement 參數(shù)的詞法范圍內(nèi)。只需在匿名函數(shù)定義中使用 htmlElement 變量,,就可以用這些函數(shù)封閉它。這稱為函數(shù)閉包(function closure):當在另一個函數(shù)內(nèi)定義一個函數(shù)時,,如果內(nèi)部函數(shù)使用外部函數(shù)的局部變量,JavaScript 就會用內(nèi)部函數(shù)的定義保存這些變量,。這樣的話,當外部函數(shù)返回之后,,在調(diào)用內(nèi)部函數(shù)時,,外部函數(shù)的局部變量仍然是可用的。
在這里,,當構(gòu)造方法返回之后,JavaScript 仍然保留 htmlElement 變量的值,,所以當瀏覽器調(diào)用匿名函數(shù)時,,匿名函數(shù)仍然可以使用這個變量,。這使它們能夠找到它們的 HTML 元素并通過指針引用它們的 FadingTooltip 對象,而不需要瀏覽器的幫助,。
因為函數(shù)閉包是 JavaScript 語言的一項特性,,所以它們在使用 W3C 事件模型的瀏覽器中一樣是有效的??梢岳眠@個特性將構(gòu)造方法的 htmlElement 參數(shù)值封閉在前一節(jié)定義的匿名函數(shù)中,而不使用內(nèi)置的 this 變量,。
在老式瀏覽器中連接鼠標事件
對于既不支持 W3C 事件模型,,也不支持 Internet Explorer 事件模型的老式瀏覽器,,必須使用 Netscape Navigator 早期版本提供的原始事件模型來連接事件。所有流行的瀏覽器都支持它,,而且 Web 設(shè)計人員廣泛使用它在 Web 頁面上建立動畫;但是對于實現(xiàn)更復(fù)雜的應(yīng)用程序,,這是最后的選擇,,因為它不能鏈接多個事件處理器。為此,,需要將以前注冊的事件函數(shù)的指針封閉在自己的事件函數(shù)定義中,,然后在調(diào)用自己的 handleEvent 方法之后調(diào)用它們,見清單 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‘ ...
}
}
|
但是注意,這種方法是不完整的,。它允許部件注冊其他部件已經(jīng)注冊的相同事件,,然后將它們鏈接在一起,,但是不允許注銷其他事件函數(shù),因為鏈接的指針對于它們不可訪問,。
為了適應(yīng)性更強,代碼將構(gòu)造方法的 this 變量(它指向 FadingTooltip 對象)復(fù)制到局部變量 self 中,,然后使用 self 指針在匿名函數(shù)定義中定位 FadingTooltip 對象,。這就把 FadingTooltip 對象的指針封閉在匿名函數(shù)定義中,所以當任何瀏覽器調(diào)用它們時,,它們都可以直接定位 FadingTooltip 對象,,而不依靠瀏覽器提供 HTML 元素的指針,也不需要將 FadingTooltip 對象的指針存儲在 HTML 元素中,。
對于為 W3C 和 Microsoft 事件模型定義的匿名函數(shù),,都可以將 FadingTooltip 對象的指針封閉在其中。這樣就不必將對象的指針保存在 HTML 元素中,,并可以在所有事件模型中應(yīng)用同樣的 HTML 元素定位技術(shù),。源代碼 中的構(gòu)造方法就采用這種方法,。
既然已經(jīng)連接了所有流行的瀏覽器中的鼠標事件,,對象構(gòu)造方法已經(jīng)完整了,,可以返回到對象原型了,。
設(shè)置計時器并連接計時器事件
我們已經(jīng)完成了 FadingTooltip 構(gòu)造方法,可以繼續(xù)填充它的原型,。在 JavaScript 中,,對象原型可以包含方法和變量;方法僅僅是指向函數(shù)的變量,。首先定義一些通用的方法,,它們啟動和取消計時器。
在第 1 部分中的設(shè)計階段提到過,,JavaScript 提供兩種類型的計時器:一次定時器和重復(fù)斷續(xù)器,,有限狀態(tài)機需要這兩種定時器。可以通過調(diào)用 setTimeout 或 setInterval 函數(shù)啟動計時器,,傳遞的參數(shù)是一個時間值(以毫秒為單位)以及當發(fā)生 timeout 或 timetick 事件時要調(diào)用的函數(shù),。它們返回不透明度的引用,可以將這些引用傳遞給 clearTimeout 或 clearInterval 函數(shù)來取消計時器,。
當超過 timeout 值指定的時間時,,或者在每次到達 timetick 時間間隔時,瀏覽器將調(diào)用傳遞給 setTimeout 和 setInterval 函數(shù)的計時器事件函數(shù)(對于 timetick ,,這個過程一直重復(fù)到取消計時器為止),。但是,,這些 timeout 和 timetick 函數(shù)不會成為任何對象的方法。當瀏覽器調(diào)用它們時,,this 變量指向全局的 window 對象,。瀏覽器并不將關(guān)于計時器事件的任何信息傳遞給這些函數(shù),。
學(xué)會處理 鼠標事件 之后,連接計時器事件也就不困難了,。當設(shè)置計時器時,,將內(nèi)置的 this 變量(它指向包含狀態(tài)變量的 FadingTooltip 對象)復(fù)制到局部變量 self 中。self 變量處于 setTimeout 和 setInterval 函數(shù)調(diào)用的詞法范圍,。然后,,定義使用 self 變量的匿名函數(shù),并將它們作為參數(shù)傳遞給 setTimeout 和 setInterval 函數(shù),。這將 self 變量封閉在函數(shù)定義中,,所以當瀏覽器調(diào)用函數(shù)時它仍然可用,見清單 6,。
清單 6. 設(shè)置計時器并連接計時器事件的 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);
},
...
|
計時器事件函數(shù)沒有鼠標事件函數(shù)那么復(fù)雜,。它們僅僅創(chuàng)建一個簡單的計時器事件對象,其中只包含一種事件類型 —— timeout 或者 timetick ,,并將它傳遞給處理鼠標事件的同一個 handleEvent 方法,。
創(chuàng)建操作/轉(zhuǎn)換表
在 JavaScript 中,對象原型可以包含數(shù)組等數(shù)據(jù)結(jié)構(gòu)和其他對象,,以及變量和方法,。普通數(shù)組的元素用整數(shù)作為索引,而關(guān)聯(lián)數(shù)組的元素用名稱作為索引,,而不是整數(shù),。在 JavaScript 中,關(guān)聯(lián)數(shù)組和對象僅僅是用來訪問相同數(shù)據(jù)的不同語法:可以以關(guān)聯(lián)數(shù)組元素的形式訪問對象屬性,,見清單 7,。
清單 7. 以關(guān)聯(lián)數(shù)組元素的形式訪問對象屬性的 JavaScript 代碼
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true
|
因此我們將 狀態(tài)表 實現(xiàn)為一個二維的函數(shù)關(guān)聯(lián)數(shù)組。直接使用狀態(tài)名稱和事件名稱作為索引,。數(shù)組的非空單元格指向匿名函數(shù),,這些匿名函數(shù)通過調(diào)用實用程序方法(比如啟動和取消 計時器 的函數(shù))來為事件執(zhí)行操作,然后返回下一個狀態(tài),。handleEvent 方法的代碼將使用數(shù)組語法調(diào)用這些操作/轉(zhuǎn)換函數(shù),,如清單 8 中的代碼所示。
清單 8. 調(diào)用關(guān)聯(lián)數(shù)組中存儲的匿名函數(shù)的 JavaScript 代碼
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event);
|
handleEvent 方法以關(guān)聯(lián)數(shù)組的形式訪問 actionTransitionFunctions 表,,使用當前狀態(tài)和事件類型作為索引,,并選擇要調(diào)用的函數(shù)。它將事件對象作為參數(shù)傳遞給這個函數(shù),。這個函數(shù)將執(zhí)行所需的操作,,然后返回下一個狀態(tài)的名稱。
因為關(guān)聯(lián)數(shù)組是對象(反之亦然),,所以可以使用對象語法定義 actionTransitionFunctions 表,但是 handleEvent 方法將使用數(shù)組語法訪問它,。例如,,在 Inactive 的初始狀態(tài)中,可能出現(xiàn)的惟一事件是 mouseover ,,所以可以定義一個處理此情況的函數(shù),,見清單 9。
清單 9. 將匿名函數(shù)存儲為對象屬性的 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 ,,其值是一個函數(shù),。當在 Inactive 狀態(tài)下發(fā)生 mouseover 事件時,handleEvent 方法將調(diào)用這個函數(shù),。它需要一個名為 event 的參數(shù),,通過調(diào)用三個實用程序函數(shù)來執(zhí)行三個操作,,然后返回 Pause 作為下一個狀態(tài)的名稱。操作包括保存鼠標位置(這是瀏覽器存儲在鼠標事件對象中的)和啟動計時器,,其超時值是一個名為 pauseTime 的參數(shù)(以秒作為單位,,所以按照 startTimer 方法的要求,將它轉(zhuǎn)換為毫秒),。
部件在 Pause 狀態(tài)下需要響應(yīng)三個事件:mousemove 、mouseout 和 timeout 事件,。在 actionTransitionFunctions 表中定義一個 Pause 對象,,它具有分別對應(yīng)于這些事件類型的屬性,如清單 10 所示,。
清單 10. 在 Pause 狀態(tài)下響應(yīng)鼠標事件的函數(shù)的 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 狀態(tài)下發(fā)生 mousemove 事件時,,handleEvent 方法將調(diào)用一個函數(shù),這個函數(shù)簡單地調(diào)用 doActionTransition 方法,,傳遞 event 參數(shù),,并返回它所返回的值。與 handleEvent 方法相似,,doActionTransition 方法使用它的前兩個參數(shù)作為數(shù)組索引訪問 actionTransitionFunctions 表,,并將它的第三個參數(shù)傳遞給在數(shù)組中找到的函數(shù)。當發(fā)生 mouseout 事件時,,代碼調(diào)用一個函數(shù),,它會取消本節(jié)前面啟動的計時器,然后轉(zhuǎn)換回 Inactive 狀態(tài),。
當發(fā)生 timeout 事件時,,將取消任何正在運行的計時器,創(chuàng)建一個初始不透明度為 0 的工具提示,,啟動一個斷續(xù)器,,并轉(zhuǎn)換到 FadeIn 狀態(tài)。
與 actionTransitionFunctions 表中的其他函數(shù)一樣,,定義一個在 FadeIn 狀態(tài)下處理 timetick 事件的函數(shù),,見清單 11。
清單 11. 在 FadeIn 狀態(tài)下響應(yīng)計時器事件的函數(shù)的 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 狀態(tài)下發(fā)生 timetick 事件時,,handleEvent 方法將調(diào)用一個函數(shù),,它略微增加工具提示的不透明度。淡入時間(以秒為單位指定),、動畫速率(不透明度從 0 開始增加的速度,,用每秒的步數(shù)指定)和最大不透明度(指定為 0.0 到 1.0 之間的浮點數(shù))都是參數(shù)。這個函數(shù)將返回當前狀態(tài),,讓有限狀態(tài)機保持在 FadeIn 狀態(tài),,直到工具提示的不透明度到達最大不透明度參數(shù),。然后,它取消斷續(xù)器,,啟動一個計時器來顯示工具提示,,并轉(zhuǎn)換到 Display 狀態(tài)。
以相似的方式定義 actionTransitionFunctions 表中的其他函數(shù),。細節(jié)請參考完整的 源代碼(其中有很多注釋),并參照 圖 1,。
實現(xiàn)事件處理器
我們已經(jīng)多次提到 handleEvent 方法,,所以它的實現(xiàn)應(yīng)該并不神秘了,見清單 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 表的實際實現(xiàn)與 前一節(jié) 中的建議不太一樣,。這個方法使用當前狀態(tài)和事件類型作為關(guān)聯(lián)數(shù)組的索引,,從 actionTransitionFunctions 表中選擇要調(diào)用的函數(shù),。但是,,這個方法將所選函數(shù)的指針復(fù)制到一個局部變量中,,然后用 function 對象的 call 方法調(diào)用這個函數(shù)。而不是直接調(diào)用它,。能夠這樣做是因為,,與其他值一樣,,function 對象可以賦值給變量。必須這樣做是因為,,當執(zhí)行函數(shù)時,,內(nèi)置的 this 變量需要指向 FadingTooltip 對象。如果像前面建議的那樣,,使用數(shù)組索引從 actionTransitionFunctions 表直接調(diào)用函數(shù),,this 變量就會指向這個表,。function 對象的 call 方法會將 this 設(shè)置為它的第一個參數(shù),,然后調(diào)用函數(shù),,傳遞其余的參數(shù)。
請記住,,actionTransitionFunctions 表是稀疏的,;為每個狀態(tài)下期望出現(xiàn)的事件定義函數(shù),其他單元格都空著,。handleEvent 方法通過調(diào)用 unexpectedEvent 方法來處理任何不期望出現(xiàn)的事件,。如果某個操作/轉(zhuǎn)換函數(shù)返回不屬于有效狀態(tài)的值,它將調(diào)用 undefinedState 方法,。這些方法將取消任何正在運行的計時器,,如果已經(jīng)創(chuàng)建了工具提示,,就刪除它,并將有限狀態(tài)機返回到初始狀態(tài)。一個方法見清單 13,;另一個方法幾乎是相同的。
清單 13. 不期望出現(xiàn)的事件的處理器的 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;
},
...
|
這些方法將顯示一個描述錯誤的警告對話框,,希望用戶將錯誤描述發(fā)給代碼的作者。
最終顯示工具提示
除了工具提示本身之外,,所有東西都實現(xiàn)了,,現(xiàn)在不用再等了。
當在 Pause 狀態(tài)下出現(xiàn) timeout 事件時,,希望工具提示出現(xiàn)在鼠標光標附近,,但是瀏覽器沒有將鼠標位置傳遞給計時器事件。幸運的是,,瀏覽器會將鼠標位置傳遞給鼠標事件,,所以當發(fā)生鼠標事件時,可以調(diào)用 saveCursorPosition 方法將它保存在狀態(tài)變量中,見清單 14,。
清單 14. 保存鼠標位置的 JavaScript 代碼
FadingTooltip.prototype = {
...
saveCursorPosition: function(event) {
this.lastCursorX = event.clientX;
this.lastCursorY = event.clientY;
},
...
|
工具提示是一個 HTML div 元素,,其中可以包含任何文本、圖像和標記,,它在 tooltipContent 參數(shù)中傳遞給構(gòu)造方法,。createTooltip 方法見清單 15。
清單 15. 創(chuàng)建工具提示的 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 = ‘a(chǎn)uto‘;
this.tooltipDivision.style.border = ‘thin solid black‘;
this.tooltipDivision.style.padding = ‘5px‘;
this.tooltipDivision.style.backgroundColor = ‘yellow‘;
}
this.tooltipDivision.style.position = ‘a(chǎn)bsolute‘;
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);
},
...
|
如果在參數(shù)中指定了 CSS 類名,,就應(yīng)用它控制 HTML div 元素的外觀。否則,,就應(yīng)用默認的基本樣式,。但是工具提示的幾個方面依賴于它的外觀,,比如它的位置和不透明度,所以要覆蓋與這些屬性相關(guān)的任何樣式,,這可以在樣式表中指定。HTML div 元素將用絕對坐標定位在頁面上,,接近最近保存的鼠標位置,,在任何重疊的其他元素上面。它的初始不透明度是 0,,即完全透明。
每當在 FadeIn 或 FadeOut 狀態(tài)下發(fā)生 timetick 事件時,,分別調(diào)用 fadeTooltip 方法略微增加或減少工具提示的不透明度,,同時確保不透明度處于 0 和最大不透明度參數(shù)之間,見清單 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;
},
...
|
操作/轉(zhuǎn)換函數(shù)也需要移動和刪除工具提示的實用程序方法,。它們的實現(xiàn)非常簡單明了,,可以通過 源代碼文件 中的注釋理解它們。
正如在本文的這一部分中提到的,,需要定義參數(shù)才能完成實現(xiàn),。它們是對象原型的屬性,但是與狀態(tài)變量不同,,它們具有清單 17 所示的默認值,。
清單 17. 在對象原型中定義參數(shù)的 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
...
};
|
對象構(gòu)造方法的可選參數(shù) parameters 是一個用 JavaScript Object Notation(有時稱為 JSON)編寫的對象,它可以覆蓋這些屬性的默認值,,見清單 18,。
清單 18. 在對象構(gòu)造方法中進行參數(shù)初始化的 JavaScript 代碼
function FadingTooltip(htmlElement, tooltipContent, parameters) {
...
for (parameter in parameters) {
if (typeof(this[parameter])!=‘undefined‘)
this[parameter] = parameters[parameter];
}
...
};
|
構(gòu)造方法在它的 parameters 參數(shù)中檢查每個屬性;對于每個屬性,,如果它存在于原型中,,那么它的值覆蓋參數(shù)的默認值。請記住,,原型是一個對象,,所以它也是一個關(guān)聯(lián)數(shù)組。這里同樣使用對象表示法定義參數(shù),,但是用數(shù)組表示法訪問它們,。
現(xiàn)在,F(xiàn)adingTooltip 的實現(xiàn)已經(jīng)完成了。您可以 下載 構(gòu)造方法和原型的源代碼,。
關(guān)于性能的幾點說明
在對實現(xiàn)進行測試之前,,要對性能做幾點說明。
瀏覽器同步地執(zhí)行 JavaScript 程序,。當連接的事件發(fā)生時,,瀏覽器調(diào)用它的事件處理器,并等待它返回,,然后再繼續(xù)處理下一個事件,。如果在事件處理器返回之前發(fā)生了更多的事件,瀏覽器就將它們放在隊列中,;當事件處理器返回時,,依次同步地處理排隊的事件,每次一個,。如果一個事件處理器花費了過長時間,,它可能會延遲瀏覽器本身對未連接的事件的響應(yīng)。用戶就可能認為程序反應(yīng)緩慢,,或者認為瀏覽器出了故障,。
所以一定要使事件處理器盡可能簡短,這在用密集的計時器事件模擬動畫的程序中尤其重要,。如果 timetick 事件處理器花費的時間超過了斷續(xù)器的時間間隔,,timetick 事件就會在瀏覽器的事件隊列中積累起來,導(dǎo)致處理器飽和并使瀏覽器反應(yīng)緩慢,。
例如,,假設(shè)動畫的默認速率是每秒 24 步,一個 timetick 事件處理器在返回到瀏覽器之前,,有差不多 40 毫秒時間完成它需要的操作(假設(shè)它占用全部處理器時間),。在現(xiàn)代的工作站上,,這段時間足夠進行許多處理,。但是,我們的目標不是在這段時間內(nèi)做盡可能多的工作,,而是使用盡可能少的處理器時間,。如果程序?qū)崿F(xiàn)的處理器使用率非常低,那么即使在其他活動的負載很高的處理器上,,動畫效果也會平滑地運行,,程序能夠做出正常的響應(yīng)。
不要將動畫速率設(shè)置為每秒 60 或 85 步(因為您認為動畫速率與顯示器的刷新頻率匹配會產(chǎn)生更平滑的動畫),。這會將 timetick 事件之間的時間減少到大約 12 毫秒,。如果 timetick 事件處理器花費的時間超過這個值,或者有其他活動爭奪處理器,那么動畫可能變得不平滑,,或者瀏覽器變得響應(yīng)緩慢,。
準備測試
完成了實現(xiàn)之后,就要在一些瀏覽器中對代碼進行測試了,。這是本系列第 3 部分的主題,。不過請記住,開發(fā)是個反復(fù)的過程,,有時可能需要返回設(shè)計或?qū)崿F(xiàn)階段...
下載
參考資料
學(xué)習(xí)
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文 ,。
- Ajax: A New Approach to Web Applications:閱讀 Jesse James Garrett 的這篇介紹 Ajax 的大作。
- JavaScript: The Definitive Guide 一書(David Flanagan,,1996 年至 2006 年由 O‘Reilly Media 多次再版):獲得有關(guān)如何能讓 JavaScript 工作于瀏覽器的詳盡信息,。
- Standard ECMA-262: ECMAScript Language Specification(Ecma International,1999):研讀可由流行的瀏覽器實現(xiàn)的 JavaScript 語言規(guī)范的權(quán)威定義,。
- Document Object Model (DOM) Level 2 Events Specification(W3C,,2000):參考 DOM Level 2 事件模型規(guī)范的權(quán)威定義。
- Gecko DOM Reference(Mozilla):獲得由 Firefox 瀏覽器實現(xiàn)的對象接口(包括事件)的權(quán)威定義,。
- HTML and DHTML Reference(Microsoft):參考由 Internet Explorer 瀏覽器實現(xiàn)的對象接口(包括事件)的權(quán)威定義,。
- 閱讀 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 等),,獲得應(yīng)用于計算機網(wǎng)絡(luò)協(xié)議的有限狀態(tài)機的一些歷史示例。
- Compilers: Principles, Techniques, ad Tools(Alfred V. Aho 等,,Addison-Welsley,,1986 年)一書中的第 3.5 章 “Finite Automata” 描述了如何將有限狀態(tài)機應(yīng)用到計算機語言編譯器。
- Design Patterns: Elements of Reusable Object-Oriented Software(Erich Gamma 等,,Addison-Welsley,,1995 年)一書中的第 5 章 “Behavioral Patterns” 討論了實現(xiàn)有限狀態(tài)機涉及到的狀態(tài)模式。
- 閱讀 Internetworking with TCP/IP(Douglas E. Comer,,Simon and Schuster Company,,1995 年)一書中的第 13.25 章 “TCP State Machine”,了解互聯(lián)網(wǎng)底層的有限狀態(tài)機,。
- Unified Modeling Language 2.0 Superstructure Specification(Object Management Group,,2004 年)中的第 15 章 “State Machines” 給出了有限狀態(tài)機的一個完整的圖形表示。
- State Chart XML (SCXML): State Machine Notation for Control Abstraction(RJ Auburn 等,,W3C)給出了以 XML 形式表示有限狀態(tài)機的一種提議,。
- developerWorks Web 開發(fā)專區(qū):通過專注于 Web 技術(shù)的文章和教程擴展您的站點開發(fā)技能。
- technology bookstore:瀏覽有關(guān)這些主題和其他技術(shù)主題的書籍,。
- developerWorks 技術(shù)事件和網(wǎng)絡(luò)廣播:關(guān)注和了解技術(shù)的最新發(fā)展,,縮短學(xué)習(xí)過程,,改進最困難的軟件項目的質(zhì)量和結(jié)果。
|