有限狀態機很早就已用作設計和實現事件驅動的程序(比如網絡適配器和編譯器)內復雜行為的組織原則。現在,可編程的 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 元素。
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'>
樣例頁面 展示了瀏覽器如何呈現具有
title
屬性的 HTML 元素。注意當光標在元素上移動時工具提示是如何出現和消失的。文本框包含簡單的文本,這些文本無任何格式和樣式。文本框會在光標短暫停留時彈出,并會在特定時間過后、鼠標從此 HTML 元素移出或單擊了某鍵的情況下突然消失。瀏覽器一次只顯示一個文本框。工具提示的外觀和行為已經硬性設定到瀏覽器內,無法更改。內置的工具提示還有很多可待提高之處,一些流行瀏覽器的最新版本為構建更為精致的工具提示提供了所需的 “原料”。HTML Division 元素創建了一個可在瀏覽器窗口的任何地方放置的提示框。通過級聯樣式表(CSS),您幾乎可以設定框體外觀的各個方面。用 JavaScript 編程實現的光標移動可以觸發瀏覽器窗口內任意可視元素的特定動作。您還可以編制一個定時器來控制這些動作的順序。
在 樣例頁面 可以找到具有這類工具提示的一些 HTML 元素。如果運行的是流行瀏覽器的最新版本,您就可以將更為精致的工具提示和內置的工具提示做一對比:
如果使用的是較老的瀏覽器,您可能無法獲得其中的一部分行為。例如,Opera 瀏覽器版本 9 之前的版本,工具提示是彈入彈出的,而非淡入淡出的,原因是 Opera 在實現 opacity
樣式屬性方面起步相對較晚。要下載流行的瀏覽器的當前版本,請參考 參考資料。
- 這類工具提示是淡入淡出的,而不是突然彈出和突然消失。
- 這類工具提示包含圖像和文本,并經很好的格式化和樣式化處理。
- 可見時,這類工具提示可以隨光標移動。
- 當光標從 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 是此過程相應的圖形表示,其中的氣球形圓圈代表狀態,連接圓圈的箭頭線代表轉移,箭頭線上的標注代表事件。雙層邊界的圓圈代表初始狀態。
FadingTooltip 小部件必須針對它處理的每個事件采取動作:
- 當 mouseover 事件在 Inactive 狀態發生時,在轉入 Pause 狀態等待之前,它必須要開啟一個一次定時器。
- 當 timeout 事件發生時,在轉入 FadeIn 狀態等待之前,它必須要創建工具提示(初始不透明度值為零)并開啟一個重復斷續器。
- 每次發生 timetick 事件,它都要適當增加工具提示的不透明度。當達到工具提示的最大不透明度時,它必須在轉入 Display 狀態等待之前取消此重復斷續器并開啟另一個定時器。
- 當定時器的 timeout 事件發生時,它必須在轉入 FadeOut 狀態等待之前開啟另一個重復斷續器。
- 每次在 FadeOut 狀態發生 timetick 事件時,它都必須要適當減少工具提示的不透明度。當工具提示的不透明度減少到零時,小部件會取消此重復斷續器,刪除工具提示并返回到 Inactive 狀態,在該狀態等待被另一個 mouseover 事件激活。
圖 2 在觸發這些動作的事件之下列出了這些動作。
回頁首
上述的狀態圖是設計有限狀態機的一個很好的開始。但表形式更適合于完成設計,原因是表可以給出事件和狀態的所有組合以供參考。
要將狀態圖轉換成狀態表,可以在行標題內填上事件名,在列標題內填上狀態名。這些名字的順序是任意的;我在第一行的開始位置放入了初始狀態,在第一列的開始位置放入了初始事件,隨后將動作和每一事件的下一狀態復制到表中適當的單元格內,如 圖 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 事件會在當前值的基礎之上減少工具提示的不透明度。在適當的單元格內記錄這些動作和轉移并將此列的所有其他單元格標記為“不應發生”。
- Display 狀態
- 光標仍可以到處移動。如果光標在 HTML 元素之內移動,采取與 FadeIn 狀態相同的動作 —— 相應移動工具提示。如果光標從 HTML 元素移出,就采取與 Display 狀態下的 timeout 事件相同的狀態和轉移。在 mousemove 和 mouseout 對應的單元格直接放上相同的內容并將此列的所有其他單元格標記為“不應發生”;
- FadeOut 狀態
- 在這種狀態下,在用 timetick 事件處理淡出時,光標仍可繼續到處移動。如果光標在 HTML 元素之內移動,采取與 FadeIn 和 Display 狀態相同的動作。如果光標從 HTML 元素移出,不需要做任何事情 —— 重復斷續器會繼續運行以便后續的 timetick 事件會在當前值的基礎之上減少工具提示的不透明度直到其值為零。
不要將此單元格標記為“不應發生”,而是應該標示為無需任何動作。如果光標又再次回到該 HTML 元素,將工具提示移回光標并返回 FadeIn 狀態。
圖 4 顯示了所有這些動作和轉移。剩下的空白單元格應標記為“不應發生”。
有限狀態機的狀態表總是能轉換回狀態圖,因為二者是等價的。圖 5 顯示了完整的狀態表對應的狀態圖。
回頁首
完成狀態表和狀態圖之后,很有必要對它再進行一次回顧來收集狀態機在兩事件間需要記錄的變量以便狀態機能夠執行不同的單元格內的相應動作。有限狀態機需要 清單 2 中所示的狀態變量。
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 變量本身不區分類型,但變量所包含的值是區分類型的(這就是說,任何類型的值都可以賦給變量)。根據這一原則,我列出了狀態變量名并在注釋部分給出了希望賦給這些變量的值的類型。
下載
- Demo: 瀏覽器工具提示和 FadingTooltip 小部件樣例
- Code sample: 瀏覽器工具提示和 FadingTooltip 小部件樣例的 HTML 源代碼
- Code sample: 瀏覽器工具提示和 FadingTooltip 小部件樣例的 JavaScript 源代碼