天天看點

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)

有限狀态機很早就已用作設計和實作事件驅動的程式(比如網絡擴充卡和編譯器)内複雜行為的組織原則。現在,可程式設計的 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 代碼

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 元素。如果運作的是流行浏覽器的最新版本,您就可以将更為精緻的工具提示和内置的工具提示做一對比:

  • 這類工具提示是淡入淡出的,而不是突然彈出和突然消失。
  • 這類工具提示包含圖像和文本,并經很好的格式化和樣式化處理。
  • 可見時,這類工具提示可以随光标移動。
  • 當光标從 HTML 元素移出然後又移回此元素時,淡入淡出會反轉方向。
  • 同時可有多個工具提示可視,一些淡出,一些淡入。

這些增強的行為和外觀不僅有修飾的作用,還可以提高可用性。面對有數十個或數百個元素的繁忙頁面,使用者很可能會錯過即刻彈出的工具提示。人類的視覺系統對運動的物體十分敏感,因而也更容易注意到淡入視野并随滑鼠而動的工具提示,即使使用者的注意力不在這兒也沒關系。對比未格式化過的文本,圖像、格式化和樣式化能更有效地傳遞資訊。而且,這些更為精緻的工具提示的所有參數都是可配置的。

本文後面的内容将着重于介紹如何将 FadingTooltip 小部件設計為一個有限狀态機。本系列的後續文章會為您展示如何實作和測試這些代碼。如果您急于想知道這些代碼,也可以在 參考資料 部分找到到相關 JavaScript 源代碼和使用這些代碼的一個 HTML Web 頁面的連結。

有限狀态機

有限狀态機對行為模組化,在該模型中,對将來事件的響應取決于先前的事件。此領域已出現了大量學術著作(參見 參考資料),而有限狀态機的實用定義卻十分簡單明了。有限狀态機就是包含如下内容的計算機程式:

  • 事件:程式對事件進行響應。
  • 狀态:程式在事件間的狀态。
  • 轉移:對應于事件,狀态間的轉移。
  • 動作:轉移過程中采取動作。
  • 變量:變量儲存事件間的動作所需的值。

在行為由許多不同類型事件驅動以及對特定事件的響應取決于先前事件發生順序的情況下,有限狀态機最為有用。 驅動有限狀态機的事件可以是計算機外部的(由鍵盤、滑鼠、定時器或網絡活動發起),也可以是計算機内部的(由本應用程式的其他部分或其他應用程式發起)。

狀态是記起先前事件的一種方式,轉移則用來組織對将來事件的響應。其中的一個狀态必須要被指派為初始狀态。結束狀态可有可無,FadingTooltip 小部件就沒有結束狀态。

有限狀态機的兩種常見表示為:

方向圖
氣球狀的圓圈代表狀态,圓圈間的箭頭線代表轉移,它會被标以事件和動作。
二維表
表的行和列代表事件和狀态,單元格内包含動作和轉移。

上述兩種表示是等價的,分别側重于設計的不同方面。兩者都十分有用,我在本文的後面都會用到。

用有限狀态機開發事件驅動程式比一般的過程式程式設計要複雜一些;一般來說,需要更多的規則,尤其是更多的設計精力。如果處理得當,有限狀态機可以使代碼簡單、測試迅速、維護輕松。但是,即便如此,有限狀态機的複雜性使其并不能适合所有事件驅動的程式的開發。例如,當事件的種類不多或事件觸發的動作總是相同時,進行額外的開發可能會得不償失。

有限狀态機和運作時環境

有限狀态機是事件驅動的,需要在它們的運作時環境将其與其相關的事件挂接起來。這可通過事件處理程式 實作,事件處理程式是一些可插入到運作時環境的小的代碼片段,一旦特定事件發生,這些處理程式就會執行。事件處理程式執行時,需要獲得如下一些基本資訊:

  • 已發生事件的類型(例如,光标移動、定時器逾時)
  • 事件的上下文(例如,光标位于哪個 HTML 元素之上、完成的是哪個網絡請求)
  • 有限狀态機自身的變量和方法的位置

JavaScript 十分适合于建構事件驅動的有限狀态機。事實上,JavaScript 有點太過适合 —— 它有三種挂接事件的方式。每種事件模型 都很直覺明了,但程式必須實作所有三種模型以確定它們可以運作于所有流行的浏覽器之上。事件的上下文在其中兩個事件模型内被直接傳遞給事件處理程式;對于另外一個模型,JavaScript 函數閉包允許事件的上下文被包裹進其事件處理程式。

JavaScript 提供一種對象模型,對象模型是 Java 和 C++ 程式員所熟知的,它也可用來對有限狀态機的變量和方法進行編碼。而且,JavaScript 關聯數組還允許直接對有限狀态機的二維表進行編碼。

系統地設計行為

有限狀态機的基本要素是它所響應的事件及事件間的狀态。設計必須考慮到每個可能狀态的每個可能事件:

  • 在該狀态下,此事件是否可能發生
  • 采取什麼動作來處理事件
  • 事件過後轉移到什麼狀态
  • 在事件之間需要記錄什麼變量

我以 圖 1 所示的一個圖形來開始設計的過程,圖中氣球形圓圈所示的是狀态,連接配接這些圓圈的箭頭線代表的是轉移。最終獲得的是一張表,如 圖 4 所示,在該表的标題行和标題列分别列出了事件和狀态。表中的一些單元格列出了當特定事件在特定狀态發生時所要執行的動作,其它一些則表示在該狀态下此事件不能發生。

通常,需要反複執行此設計過程才能獲得正确的圖和表。對具有多個事件和狀态的有限狀态機,這個過程可能會十分乏味,每次重複都需要遵守一定的原則來系統地處理表中的每一個單元格。這迫使您不得不考慮在每個可能的情況下您所想要的動作。您可能會發現還可以進一步完善這些行為,也可能會發現所需的狀态較預計的要多(或少),甚至會發現您必須重新整理單元格間的這些動作以正确定義每種情況下的行為。

這種設計有限狀态機的系統過程雖然有些乏味但卻十分值得。圖 4 所示的完成後的表給出了此行動的所有邏輯,并可被直接轉換為代碼(參見 actionTransitionFunctions 源代碼)。

關于 JavaScript

要設計 FadingTooltip 小部件,您需要了解 JavaScript 的一些功能。在嚴謹設計的原則指導下,我隻在這裡給出基本的設計思想,而将具體的實作留待本系列後續文章中介紹。

當光标經過頁面中的 HTML 元素時,所有流行的浏覽器都能将事件傳遞給 JavaScript 代碼。這些事件是 mouseover、mousemove 和 mouseout,分别代表光标已經移至、移上和移出 HTML 元素。浏覽器用這些事件傳遞光标目前位置。當事件發生時,可用 JavaScript 程式設計動态建立 HTML Division 元素,用文本、圖像和标記填充這些元素并将其定位到光标附近。

浏覽器并沒有原生的淡入和淡出函數,但可以通過改變 Division 元素的透明度(實際上是不透明度,透明度的反義詞)來模拟這些函數。

JavaScript 有兩類定時器:一次定時器在逾時時生成 timeout 事件;重複斷續器定期生成 timetick 事件。FadingTooltip 小部件需要這兩種定時器。

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
回頁首

設計狀态圖

首先回顧一下想要從 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. 狀态圖的初始設計

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)

FadingTooltip 小部件必須針對它處理的每個事件采取動作:

  • 當 mouseover 事件在 Inactive 狀态發生時,在轉入 Pause 狀态等待之前,它必須要開啟一個一次定時器。
  • 當 timeout 事件發生時,在轉入 FadeIn 狀态等待之前,它必須要建立工具提示(初始不透明度值為零)并開啟一個重複斷續器。
  • 每次發生 timetick 事件,它都要适當增加工具提示的不透明度。當達到工具提示的最大不透明度時,它必須在轉入 Display 狀态等待之前取消此重複斷續器并開啟另一個定時器。
  • 當定時器的 timeout 事件發生時,它必須在轉入 FadeOut 狀态等待之前開啟另一個重複斷續器。
  • 每次在 FadeOut 狀态發生 timetick 事件時,它都必須要适當減少工具提示的不透明度。當工具提示的不透明度減少到零時,小部件會取消此重複斷續器,删除工具提示并傳回到 Inactive 狀态,在該狀态等待被另一個 mouseover 事件激活。

圖 2 在觸發這些動作的事件之下列出了這些動作。

圖 2. 在初始狀态圖的事件下追加動作

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
回頁首

将狀态圖轉換成狀态表

上述的狀态圖是設計有限狀态機的一個很好的開始。但表形式更适合于完成設計,原因是表可以給出事件和狀态的所有組合以供參考。

要将狀态圖轉換成狀态表,可以在行标題内填上事件名,在列标題内填上狀态名。這些名字的順序是任意的;我在第一行的開始位置放入了初始狀态,在第一列的開始位置放入了初始事件,随後将動作和每一事件的下一狀态複制到表中适當的單元格内,如 圖 3 所示。

圖 3. 與初始狀态圖對應的初始狀态表

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
回頁首

完成狀态表

要完成有限狀态機的設計,需要顧及表中的每一個空單元格。您需要為每個單元格做這樣的考慮:該事件是否可以發生在該狀态,如果可以,小部件在這種情況下将采取什麼動作,下一個狀态又将是什麼。這雖然有些乏味,但卻是設計過程的必需部分。

考慮單元格的順序先後關系不大。通常在設計過程中需要多次重複此步驟,反複考慮每個單元格,不時地修改其内容,而且每次的考慮順序都會有所不同。另外随着設計的不斷深入,添加(或删除)狀态、做進一步的修改也十分常見。在這裡,我将跳過這些反複過程,着重總結如何通過依次檢視每個狀态和事件來獲得最終的結果表。

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 顯示了所有這些動作和轉移。剩下的空白單元格應标記為“不應發生”。

圖 4. FadingTooltip 小部件設計後的狀态表

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)

有限狀态機的狀态表總是能轉換回狀态圖,因為二者是等價的。圖 5 顯示了完整的狀态表對應的狀态圖。

圖 5. FadingTooltip 小部件設計後的狀态圖

JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
JavaScript 中的有限狀态機,第 1 部分 (轉自IBM DevWork)
回頁首

收集狀态變量

完成狀态表和狀态圖之後,很有必要對它再進行一次回顧來收集狀态機在兩事件間需要記錄的變量以便狀态機能夠執行不同的單元格内的相應動作。有限狀态機需要 清單 2 中所示的狀态變量。

清單 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 變量本身不區分類型,但變量所包含的值是區分類型的(這就是說,任何類型的值都可以賦給變量)。根據這一原則,我列出了狀态變量名并在注釋部分給出了希望賦給這些變量的值的類型。

繼續閱讀