多年以來(lái),Web 設計人員一直都通過(guò)在流行的 Web 瀏覽器內采用 JavaScript 解釋器的方式來(lái)改善其網(wǎng)站的外觀(guān)。他們的做法大都是將代碼的簡(jiǎn)短片段復制到 HTML 頁(yè)面中。當前,隨著(zhù) Ajax 的日益流行,軟件工程師也開(kāi)始使用 JavaScript 來(lái)開(kāi)發(fā)能在瀏覽器內執行的新一代的應用程序?;跒g覽器的應用程序的規模不斷擴大,這就相應要求采用其他執行環(huán)境成長(cháng)和發(fā)展所使用的相同設計模式和開(kāi)發(fā)原理。
![]() |
|
基于瀏覽器的應用程序在實(shí)時(shí)環(huán)境中執行,在這種環(huán)境中鼠標、鍵盤(pán)、定時(shí)器、網(wǎng)絡(luò )和程序事件都十分常見(jiàn)。當事件驅動(dòng)的應用程序的行為取決于事件發(fā)生的順序時(shí),其編程就會(huì )變得非常復雜,也十分難以調試和修改。軟件工程師早已開(kāi)始使用 有限狀態(tài)機 —— 學(xué)術(shù)領(lǐng)域有時(shí)又稱(chēng)其為離散或確定性有限自動(dòng)機 —— 作為一種組織原理來(lái)開(kāi)發(fā)事件驅動(dòng)的程序了。
有限狀態(tài)機通過(guò)用直觀(guān)的表格代替復雜的邏輯為設計增加了嚴密性。從傳統意義上講,有限狀態(tài)機對開(kāi)發(fā)諸如網(wǎng)絡(luò )驅動(dòng)程序和編譯器這類(lèi)程序頗有幫助。有限狀態(tài)機也同樣有助于開(kāi)發(fā)基于瀏覽器的應用程序。
在本系列中,您將練習開(kāi)發(fā)一個(gè)樣例有限狀態(tài)機應用程序,來(lái)深入體驗 JavaScript 語(yǔ)言的一些獨特特性:
這些語(yǔ)言特性可以提供一種緊湊而簡(jiǎn)明的方式來(lái)為狀態(tài)間的事件和轉移組織動(dòng)作,還可以提供一種巧妙的方式來(lái)兼容不同的瀏覽器事件模型。
樣例應用程序 FadingTooltip 比內置于大多數瀏覽器的默認工具提示更為精致。用 FadingTooltip 小部件創(chuàng )建的工具提示使用動(dòng)畫(huà)式淡入和淡出代替突然彈出和消失,并可隨光標移動(dòng)。設計此行為所用的有限狀態(tài)機模式使邏輯清晰透明。實(shí)現此行為所用的 JavaScript 語(yǔ)言特性則使源代碼緊湊而有效。
本文展示了如何使用有限狀態(tài)機的圖、表表示設計一個(gè)動(dòng)畫(huà)式小部件的行為。本系列的后續文章會(huì )介紹如何在 JavaScript 內實(shí)現有限狀態(tài)機的表表示以及如何處理與在流行的瀏覽器內進(jìn)行測試和實(shí)現相關(guān)的實(shí)際問(wèn)題。
當光標暫時(shí)停留于一些可視控件 —— 比如按鈕、選擇器或輸入字段 —— 時(shí),時(shí)下的許多圖形應用程序都能暫時(shí)顯示包含相應的幫助性定義、操作說(shuō)明或建議的小文本框。在早期的系統中,這些小文本框被稱(chēng)為 “氣球幫助”,在 IBM 的一些產(chǎn)品中,稱(chēng)其為 infopop,在一些 Microsoft 產(chǎn)品中,其名字則是 ScreenTip。在本文,我使用的是其中更為常見(jiàn)的術(shù)語(yǔ)工具提示。
現在一些流行的 Web 瀏覽器,比如 Netscape Navigator、Microsoft Internet Explorer、Opera 和 Mozilla Firefox,會(huì )為任何擁有 title 屬性的 HTML 元素顯示工具提示。例如,清單 1 中顯示的這三個(gè)擁有 title 屬性的 HTML 元素。
Here are some <span title=‘Move your cursor a bit to the right, please.‘> fields with built-in tooltips </span>: <input type=‘text‘ title=‘Type your bank account and PIN numbers here, please ...‘ size=25> <input type=‘button‘ title=‘Go ahead. Press it. What‘s the harm? Trust me.‘ value=‘Press this button‘> |
樣例頁(yè)面 展示了瀏覽器如何呈現具有 title 屬性的 HTML 元素。注意當光標在元素上移動(dòng)時(shí)工具提示是如何出現和消失的。文本框包含簡(jiǎn)單的文本,這些文本無(wú)任何格式和樣式。文本框會(huì )在光標短暫停留時(shí)彈出,并會(huì )在特定時(shí)間過(guò)后、鼠標從此 HTML 元素移出或單擊了某鍵的情況下突然消失。瀏覽器一次只顯示一個(gè)文本框。工具提示的外觀(guān)和行為已經(jīng)硬性設定到瀏覽器內,無(wú)法更改。
內置的工具提示還有很多可待提高之處,一些流行瀏覽器的最新版本為構建更為精致的工具提示提供了所需的 “原料”。HTML Division 元素創(chuàng )建了一個(gè)可在瀏覽器窗口的任何地方放置的提示框。通過(guò)級聯(lián)樣式表(CSS),您幾乎可以設定框體外觀(guān)的各個(gè)方面。用 JavaScript 編程實(shí)現的光標移動(dòng)可以觸發(fā)瀏覽器窗口內任意可視元素的特定動(dòng)作。您還可以編制一個(gè)定時(shí)器來(lái)控制這些動(dòng)作的順序。
在 樣例頁(yè)面 可以找到具有這類(lèi)工具提示的一些 HTML 元素。如果運行的是流行瀏覽器的最新版本,您就可以將更為精致的工具提示和內置的工具提示做一對比:
![]() |
|
這些增強的行為和外觀(guān)不僅有修飾的作用,還可以提高可用性。面對有數十個(gè)或數百個(gè)元素的繁忙頁(yè)面,用戶(hù)很可能會(huì )錯過(guò)即刻彈出的工具提示。人類(lèi)的視覺(jué)系統對運動(dòng)的物體十分敏感,因而也更容易注意到淡入視野并隨鼠標而動(dòng)的工具提示,即使用戶(hù)的注意力不在這兒也沒(méi)關(guān)系。對比未格式化過(guò)的文本,圖像、格式化和樣式化能更有效地傳遞信息。而且,這些更為精致的工具提示的所有參數都是可配置的。
本文后面的內容將著(zhù)重于介紹如何將 FadingTooltip 小部件設計為一個(gè)有限狀態(tài)機。本系列的后續文章會(huì )為您展示如何實(shí)現和測試這些代碼。如果您急于想知道這些代碼,也可以在 參考資料 部分找到到相關(guān) JavaScript 源代碼和使用這些代碼的一個(gè) HTML Web 頁(yè)面的鏈接。
![]() ![]() |
![]()
|
有限狀態(tài)機對行為建模,在該模型中,對將來(lái)事件的響應取決于先前的事件。此領(lǐng)域已出現了大量學(xué)術(shù)著(zhù)作(參見(jiàn) 參考資料),而有限狀態(tài)機的實(shí)用定義卻十分簡(jiǎn)單明了。有限狀態(tài)機就是包含如下內容的計算機程序:
在行為由許多不同類(lèi)型事件驅動(dòng)以及對特定事件的響應取決于先前事件發(fā)生順序的情況下,有限狀態(tài)機最為有用。 驅動(dòng)有限狀態(tài)機的事件可以是計算機外部的(由鍵盤(pán)、鼠標、定時(shí)器或網(wǎng)絡(luò )活動(dòng)發(fā)起),也可以是計算機內部的(由本應用程序的其他部分或其他應用程序發(fā)起)。
狀態(tài)是記起先前事件的一種方式,轉移則用來(lái)組織對將來(lái)事件的響應。其中的一個(gè)狀態(tài)必須要被指派為初始狀態(tài)。結束狀態(tài)可有可無(wú),FadingTooltip 小部件就沒(méi)有結束狀態(tài)。
有限狀態(tài)機的兩種常見(jiàn)表示為:
用有限狀態(tài)機開(kāi)發(fā)事件驅動(dòng)程序比一般的過(guò)程式編程要復雜一些;一般來(lái)說(shuō),需要更多的規則,尤其是更多的設計精力。如果處理得當,有限狀態(tài)機可以使代碼簡(jiǎn)單、測試迅速、維護輕松。但是,即便如此,有限狀態(tài)機的復雜性使其并不能適合所有事件驅動(dòng)的程序的開(kāi)發(fā)。例如,當事件的種類(lèi)不多或事件觸發(fā)的動(dòng)作總是相同時(shí),進(jìn)行額外的開(kāi)發(fā)可能會(huì )得不償失。
![]() ![]() |
![]()
|
有限狀態(tài)機是事件驅動(dòng)的,需要在它們的運行時(shí)環(huán)境將其與其相關(guān)的事件掛接起來(lái)。這可通過(guò)事件處理程序 實(shí)現,事件處理程序是一些可插入到運行時(shí)環(huán)境的小的代碼片段,一旦特定事件發(fā)生,這些處理程序就會(huì )執行。事件處理程序執行時(shí),需要獲得如下一些基本信息:
JavaScript 十分適合于構建事件驅動(dòng)的有限狀態(tài)機。事實(shí)上,JavaScript 有點(diǎn)太過(guò)適合 —— 它有三種掛接事件的方式。每種事件模型 都很直觀(guān)明了,但程序必須實(shí)現所有三種模型以確保它們可以運行于所有流行的瀏覽器之上。事件的上下文在其中兩個(gè)事件模型內被直接傳遞給事件處理程序;對于另外一個(gè)模型,JavaScript 函數閉包允許事件的上下文被包裹進(jìn)其事件處理程序。
JavaScript 提供一種對象模型,對象模型是 Java 和 C++ 程序員所熟知的,它也可用來(lái)對有限狀態(tài)機的變量和方法進(jìn)行編碼。而且,JavaScript 關(guān)聯(lián)數組還允許直接對有限狀態(tài)機的二維表進(jìn)行編碼。
![]() ![]() |
![]()
|
有限狀態(tài)機的基本要素是它所響應的事件及事件間的狀態(tài)。設計必須考慮到每個(gè)可能狀態(tài)的每個(gè)可能事件:
我以 圖 1 所示的一個(gè)圖形來(lái)開(kāi)始設計的過(guò)程,圖中氣球形圓圈所示的是狀態(tài),連接這些圓圈的箭頭線(xiàn)代表的是轉移。最終獲得的是一張表,如 圖 4 所示,在該表的標題行和標題列分別列出了事件和狀態(tài)。表中的一些單元格列出了當特定事件在特定狀態(tài)發(fā)生時(shí)所要執行的動(dòng)作,其它一些則表示在該狀態(tài)下此事件不能發(fā)生。
通常,需要反復執行此設計過(guò)程才能獲得正確的圖和表。對具有多個(gè)事件和狀態(tài)的有限狀態(tài)機,這個(gè)過(guò)程可能會(huì )十分乏味,每次重復都需要遵守一定的原則來(lái)系統地處理表中的每一個(gè)單元格。這迫使您不得不考慮在每個(gè)可能的情況下您所想要的動(dòng)作。您可能會(huì )發(fā)現還可以進(jìn)一步完善這些行為,也可能會(huì )發(fā)現所需的狀態(tài)較預計的要多(或少),甚至會(huì )發(fā)現您必須重新整理單元格間的這些動(dòng)作以正確定義每種情況下的行為。
這種設計有限狀態(tài)機的系統過(guò)程雖然有些乏味但卻十分值得。圖 4 所示的完成后的表給出了此行動(dòng)的所有邏輯,并可被直接轉換為代碼(參見(jiàn) actionTransitionFunctions 源代碼)。
![]() ![]() |
![]()
|
要設計 FadingTooltip 小部件,您需要了解 JavaScript 的一些功能。在嚴謹設計的原則指導下,我只在這里給出基本的設計思想,而將具體的實(shí)現留待本系列后續文章中介紹。
當光標經(jīng)過(guò)頁(yè)面中的 HTML 元素時(shí),所有流行的瀏覽器都能將事件傳遞給 JavaScript 代碼。這些事件是 mouseover、mousemove 和 mouseout,分別代表光標已經(jīng)移至、移上和移出 HTML 元素。瀏覽器用這些事件傳遞光標當前位置。當事件發(fā)生時(shí),可用 JavaScript 編程動(dòng)態(tài)創(chuàng )建 HTML Division 元素,用文本、圖像和標記填充這些元素并將其定位到光標附近。
瀏覽器并沒(méi)有原生的淡入和淡出函數,但可以通過(guò)改變 Division 元素的透明度(實(shí)際上是不透明度,透明度的反義詞)來(lái)模擬這些函數。
JavaScript 有兩類(lèi)定時(shí)器:一次定時(shí)器在超時(shí)時(shí)生成 timeout 事件;重復斷續器定期生成 timetick 事件。FadingTooltip 小部件需要這兩種定時(shí)器。
![]() ![]() |
![]()
|
首先回顧一下想要從 FadingTooltip 小部件獲得的基本行為。當光標從特定的 HTML 元素上移過(guò)的時(shí)候,您可能想讓此小部件等待光標在該元素上暫停。如果可以如此,之后您可能又想讓此小部件將工具提示淡入,顯示一會(huì )后再淡出。
有限狀態(tài)機將需要響應以下事件:
您將需要設計狀態(tài)機在事件間等待的一些狀態(tài)。需要調用小部件的初始狀態(tài) Inactive,小部件在該狀態(tài)下等待被 mouseover 事件激活。小部件在 Pause 狀態(tài)下等待直到 timeout 事件指示光標已經(jīng)在 HTML 元素上停留了足夠長(cháng)的時(shí)間。之后在用 timetick 事件動(dòng)畫(huà)式淡入的同時(shí),小部件會(huì )在 FadeIn 狀態(tài)下等待,繼而又會(huì )在 Display 狀態(tài)等待另一個(gè) timeout 事件。最后,在用更多 timetick 事件動(dòng)畫(huà)式淡出的同時(shí),小部件會(huì )在 FadeOut 狀態(tài)下等待。小部件轉回到 Inactive 狀態(tài),在此狀態(tài)下等待另一個(gè) mouseover 事件。
圖 1 是此過(guò)程相應的圖形表示,其中的氣球形圓圈代表狀態(tài),連接圓圈的箭頭線(xiàn)代表轉移,箭頭線(xiàn)上的標注代表事件。雙層邊界的圓圈代表初始狀態(tài)。

FadingTooltip 小部件必須針對它處理的每個(gè)事件采取動(dòng)作:
圖 2 在觸發(fā)這些動(dòng)作的事件之下列出了這些動(dòng)作。

![]() ![]() |
![]()
|
上述的狀態(tài)圖是設計有限狀態(tài)機的一個(gè)很好的開(kāi)始。但表形式更適合于完成設計,原因是表可以給出事件和狀態(tài)的所有組合以供參考。
要將狀態(tài)圖轉換成狀態(tài)表,可以在行標題內填上事件名,在列標題內填上狀態(tài)名。這些名字的順序是任意的;我在第一行的開(kāi)始位置放入了初始狀態(tài),在第一列的開(kāi)始位置放入了初始事件,隨后將動(dòng)作和每一事件的下一狀態(tài)復制到表中適當的單元格內,如 圖 3 所示。

![]() ![]() |
![]()
|
要完成有限狀態(tài)機的設計,需要顧及表中的每一個(gè)空單元格。您需要為每個(gè)單元格做這樣的考慮:該事件是否可以發(fā)生在該狀態(tài),如果可以,小部件在這種情況下將采取什么動(dòng)作,下一個(gè)狀態(tài)又將是什么。這雖然有些乏味,但卻是設計過(guò)程的必需部分。
考慮單元格的順序先后關(guān)系不大。通常在設計過(guò)程中需要多次重復此步驟,反復考慮每個(gè)單元格,不時(shí)地修改其內容,而且每次的考慮順序都會(huì )有所不同。另外隨著(zhù)設計的不斷深入,添加(或刪除)狀態(tài)、做進(jìn)一步的修改也十分常見(jiàn)。在這里,我將跳過(guò)這些反復過(guò)程,著(zhù)重總結如何通過(guò)依次查看每個(gè)狀態(tài)和事件來(lái)獲得最終的結果表。
在繼續之前,還應注意一下此狀態(tài)的 mouseover 事件。當為此工具提示創(chuàng )建 HTML Division 元素時(shí),需要將它定位于光標的附近,所以要保存光標的當前位置,當前位置由瀏覽器與此事件一同傳遞。而且在開(kāi)始新的定時(shí)器之前,最好能夠取消任何運行著(zhù)的定時(shí)器。在 mouseover 對應的單元格內添加上述動(dòng)作。
在等待定時(shí)器超時(shí)時(shí),光標可能會(huì )在 HTML 元素內移動(dòng)或從此 HTML 元素移出。需要決定一旦發(fā)生這些事件所應采取的動(dòng)作以及下一個(gè)狀態(tài)是什么。如果在此狀態(tài)發(fā)生 mouseout 事件,FadingTooltip 小部件應能返回 Inactive 狀態(tài),就像光標從未經(jīng)過(guò) HTML 元素一樣,而且還必須取消定時(shí)器。在 mouseout 對應的單元格記錄這些動(dòng)作和轉移。
另一方面,對于 mousemove 事件,則需要小部件能夠繼續等待光標懸停,這又要求取消和重新開(kāi)啟定時(shí)器。因為想要讓工具提示出現在光標的附近,所以需要更新所保存的光標位置。Pause 狀態(tài)下的 mousemove 事件的動(dòng)作和轉移與 Inactive 狀態(tài)下的 mousemove 事件的動(dòng)作和轉移相同。所以無(wú)需重復兩個(gè)單元格的內容,在 mousemove 對應的單元格內放上同樣的內容即可。將此列的所有其他單元格標記為“不應發(fā)生”。
不要將此單元格標記為“不應發(fā)生”,而是應該標示為無(wú)需任何動(dòng)作。如果光標又再次回到該 HTML 元素,將工具提示移回光標并返回 FadeIn 狀態(tài)。
圖 4 顯示了所有這些動(dòng)作和轉移。剩下的空白單元格應標記為“不應發(fā)生”。

有限狀態(tài)機的狀態(tài)表總是能轉換回狀態(tài)圖,因為二者是等價(jià)的。圖 5 顯示了完整的狀態(tài)表對應的狀態(tài)圖。

![]() ![]() |
![]()
|
完成狀態(tài)表和狀態(tài)圖之后,很有必要對它再進(jìn)行一次回顧來(lái)收集狀態(tài)機在兩事件間需要記錄的變量以便狀態(tài)機能夠執行不同的單元格內的相應動(dòng)作。有限狀態(tài)機需要 清單 2 中所示的狀態(tài)變量。
currentState string value equal to one of the state names currentTimer pointer to timer object, obtained when set, used to cancel currentTicker pointer to ticker object, obtained when started, used to cancel currentOpacity float that varies from 0.0 (invisible) to 1.0 (fully visible) lastCursorPosition floats obtained from cursor events, used when an HTML Division element is created tooltipDivision pointer to HTML Division element, set when created, used when faded, moved, or deleted |
雖然 JavaScript 變量本身不區分類(lèi)型,但變量所包含的值是區分類(lèi)型的(這就是說(shuō),任何類(lèi)型的值都可以賦給變量)。根據這一原則,我列出了狀態(tài)變量名并在注釋部分給出了希望賦給這些變量的值的類(lèi)型。
![]() ![]() |
![]()
|
![]() |
| |||||||||||||||
有了完整的狀態(tài)表和狀態(tài)變量之后,就可以實(shí)現有限狀態(tài)機了。本系列的下一篇文章將重點(diǎn)介紹實(shí)現過(guò)程,敬請關(guān)注。不過(guò)請記住開(kāi)發(fā)是個(gè)反復的過(guò)程,有時(shí)您可能需要返回設計階段。
聯(lián)系客服