有限狀態(tài)機(jī)很早就已用作設(shè)計(jì)和實(shí)現(xiàn)事件驅(qū)動的程序(比如網(wǎng)絡(luò)適配器和編譯器)內(nèi)復(fù)雜行為的組織原則?,F(xiàn)在,可編程的 Web 瀏覽器為新一代的應(yīng)用程序開辟了一種全新的事件驅(qū)動環(huán)境?;跒g覽器的應(yīng)用程序因 Ajax 而廣為流行,而同時(shí)也變得更為復(fù)雜。程序設(shè)計(jì)人員和實(shí)現(xiàn)人員能夠大大受益于有限狀態(tài)機(jī)的原理和結(jié)構(gòu)。
第 1 部分 描述 Web 頁面的一個(gè)工具提示部件,與流行的 Web 瀏覽器實(shí)現(xiàn)的內(nèi)置實(shí)現(xiàn)相比,它具有更高級的行為。當(dāng)鼠標(biāo)光標(biāo)停留在一個(gè) HTML 元素上之后,這個(gè) FadingTooltip 部件會淡入視圖,工具提示顯示一段時(shí)間之后就淡出視圖。這個(gè)工具提示會跟隨鼠標(biāo)的移動,即使在淡入和淡出期間,而且當(dāng)光標(biāo)從 HTML 元素移出然后又移回此元素時(shí),淡入淡出會反轉(zhuǎn)方向。這種行為要求 FadingTooltip 部件能夠響應(yīng)各種不同的事件,而且在某些情況下,對特定事件的響應(yīng)取決于以前發(fā)生的事件。
開發(fā)人員可以使用有限狀態(tài)機(jī)設(shè)計(jì)模式來組織這樣的事件驅(qū)動程序。在第 1 部分中,我們應(yīng)用有限狀態(tài)機(jī)的設(shè)計(jì)原理生成了一個(gè)定義所需行為的狀態(tài)表,如圖 1 所示。
狀態(tài)表的行和列標(biāo)上了部件要響應(yīng)的事件的名稱,以及在事件之間部件所處的狀態(tài)。表中的每個(gè)單元格指定,在特定狀態(tài)下發(fā)生特定事件時(shí),部件將采取的操作。表單元格還可以指定采取操作之后部件將轉(zhuǎn)移到的下一個(gè)狀態(tài),或者指定部件將保持同樣的狀態(tài)。空的表單元格表示在特定狀態(tài)下不應(yīng)該發(fā)生特定的事件。另外,在第 1 部分中,我們編寫了一個(gè) 狀態(tài)變量 列表,部件需要在事件之間記住這些變量,以便能夠執(zhí)行不同的單元格中的相關(guān)操作。
![]() |
|
第 1 部分指出 JavaScript 很適合作為有限狀態(tài)機(jī)的執(zhí)行環(huán)境,并提到它的一些與設(shè)計(jì)階段相關(guān)的功能。在本文中,學(xué)習(xí)將設(shè)計(jì)轉(zhuǎn)換為 JavaScript 的細(xì)節(jié),利用一些優(yōu)雅的語言特性,以及對一些不太優(yōu)雅的細(xì)節(jié)進(jìn)行調(diào)整以使實(shí)現(xiàn)更合理。
將設(shè)計(jì)轉(zhuǎn)換為 JavaScript
在第 1 部分中完成了有限狀態(tài)機(jī)的設(shè)計(jì)之后,就可以用 JavaScript 實(shí)現(xiàn) FadingTooltip 部件了。這是從設(shè)計(jì)階段到真實(shí)執(zhí)行環(huán)境的轉(zhuǎn)換階段,也就是從輕松的抽象轉(zhuǎn)換到實(shí)用性。
我們只考慮最流行的瀏覽器的最新版本:Netscape Navigator、Microsoft® Internet Explorer®、Opera 和 Mozilla Firefox。盡管這些執(zhí)行環(huán)境的種類并不多,但是仍然會帶來許多麻煩。我們必須處理將來自不同瀏覽器的鼠標(biāo)和計(jì)時(shí)器事件連接到 JavaScript 程序的細(xì)節(jié)。有一種優(yōu)雅的 JavaScript 語言特性稱為函數(shù)閉包(function closure),它可以幫助您簡化實(shí)現(xiàn)。還可以應(yīng)用另一個(gè)優(yōu)雅的 JavaScript 語言特性關(guān)聯(lián)數(shù)組(associative array) 將狀態(tài)表直接轉(zhuǎn)換成代碼。還會看到如何使用 HTML div 元素創(chuàng)建工具提示并指定樣式,用文本和圖像填充它,將它定位在鼠標(biāo)旁邊,使它淡入和淡出視圖,并跟隨鼠標(biāo)的移動。
但是按照面向?qū)ο箝_發(fā)的精神,首先需要一個(gè)對象,它包含將實(shí)現(xiàn)的所有東西,所以我們首先開發(fā)這個(gè)對象。
![]() ![]() |
![]()
|
Web 設(shè)計(jì)人員常常將一些簡短的 JavaScript 代碼片段復(fù)制并粘貼到 HTML 頁面中,F(xiàn)adingTooltip 部件的編程過程比這要復(fù)雜一些。軟件工程師喜歡將部件的變量和方法分組在一個(gè)對象中,但是 JavaScript 對象模型在 Java™ 和 C++ 程序員看來可能有點(diǎn)兒奇怪。一個(gè) JavaScript 對象就能夠完全滿足需要:它可以將變量和方法分組在一個(gè)對象中,然后為每個(gè)工具提示創(chuàng)建單獨(dú)的數(shù)據(jù)實(shí)例。這些對象實(shí)例將共享同樣的代碼,并獨(dú)立運(yùn)行。
在 JavaScript 中,對象構(gòu)造方法(constructor) 僅僅是一個(gè)函數(shù) —— 這個(gè)函數(shù)的名稱是對象的名稱。這個(gè)部件需要知道它自己要連接到哪個(gè) HTML 元素,以及要在工具提示中顯示什么內(nèi)容,所以要作為構(gòu)造方法的參數(shù)指定這些,并將它們保存在對象中。(還需要有辦法設(shè)置與工具提示的行為和外觀相關(guān)的參數(shù),所以也為此指定一個(gè)參數(shù),并在本文后面使用它。)變量是無類型的,所以對象構(gòu)造方法可能以清單 1 這樣的代碼開頭。
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)建對象時(shí)或者在以后任何時(shí)候,給對象添加屬性(property),屬性可以是變量或方法;創(chuàng)建屬性的辦法是將一個(gè)值賦給它們,就像這個(gè)構(gòu)造方法對 this.htmlElement
和 this.tooltipContent
屬性所做的。
在 JavaScript 中,對象原型(prototype) 是一種用來創(chuàng)建對象的新實(shí)例的模板;它定義對象的初始屬性及其初始值。我們首先在對象原型中定義第 1 部分中確定部件需要的狀態(tài)變量,見清單 2。
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)機(jī)有關(guān)的幾乎任何東西的合適位置:狀態(tài)表、它的操作及其參數(shù)。還需要加上最后一點(diǎn)兒東西就可以完成對象構(gòu)造方法 —— 連接鼠標(biāo)事件,然后本文的其余部分將致力于填充對象原型。
![]() ![]() |
![]()
|
正如在第 1 部分中的 設(shè)計(jì)階段 提到的,當(dāng)鼠標(biāo)進(jìn)入和離開 HTML 元素以及在 HTML 元素內(nèi)移動時(shí),瀏覽器可以將事件傳遞給 JavaScript。這些事件包含有幫助的信息,比如事件類型和鼠標(biāo)在頁面上的當(dāng)前位置。瀏覽器通過調(diào)用預(yù)先注冊的函數(shù)來傳遞事件。不幸的是,注冊這些函數(shù)以及將參數(shù)傳遞給它們的方式因?yàn)g覽器而異。為了確保您的有限狀態(tài)機(jī)可以連接到所有流行的瀏覽器中的鼠標(biāo)事件,需要實(shí)現(xiàn)三個(gè)不同的事件模型。好在每個(gè)事件模型的代碼都十分緊湊。不幸的是,代碼的緊湊性掩蓋了它的復(fù)雜性。
Mozilla Firefox、Opera 和 Netscape Navigator 的最新版本支持 World Wide Web Consortium(W3C)提議的 標(biāo)準(zhǔn)化事件模型(standardized event model)。這是首選的,因?yàn)楹苋菀鬃裕ê妥N)事件函數(shù),而且可以將瀏覽器處理的多個(gè)已注冊函數(shù)鏈接起來。如果可用的話,可以調(diào)用 HTML 元素的 addEventListener
方法來連接鼠標(biāo)事件,調(diào)用時(shí)要傳遞一個(gè)事件類型以及當(dāng) HTML 元素上發(fā)生此事件時(shí)調(diào)用的函數(shù),如清單 3 所示。
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)用的第二個(gè)參數(shù)是匿名函數(shù)(anonymous function),也就是沒有名稱的函數(shù)。這是在 JavaScript 中在其他函數(shù)中定義函數(shù)的第一種方法,但不是惟一的方法,目前就采用這種方法。可以在 JavaScript 代碼中的任何地方使用 function
關(guān)鍵字動態(tài)地定義匿名函數(shù)。它返回一個(gè)函數(shù)指針,可以像任何其他引用值一樣使用它們。在 FadingTooltip 部件中,將函數(shù)指針作為參數(shù)傳遞給其他函數(shù)、測試它們是否為 null
、將它們賦值給變量以及將它們聲明為對象方法。
傳遞給 addEventListener
方法的匿名函數(shù)看起來并不復(fù)雜。當(dāng)鼠標(biāo)事件發(fā)生時(shí),瀏覽器將調(diào)用它們,將 event
對象傳遞給它們,它們將傳遞給 FadingTooltip 對象的 handleEvent
方法。瀏覽器的事件對象包含事件類型以及鼠標(biāo)位置,所以一個(gè) handleEvent
方法可以處理部件必須響應(yīng)的所有鼠標(biāo)事件。
這些簡單的匿名函數(shù)還執(zhí)行另一個(gè)重要而微妙的任務(wù)。在 W3C 事件模型中,用 HTML 元素的 addEventListener
方法注冊的函數(shù)會成為這個(gè)元素的方法,所以當(dāng)瀏覽器調(diào)用它們時(shí),內(nèi)置的 this
變量會指向這個(gè) HTML 元素。但是,handleEvent
方法需要一個(gè)包含狀態(tài)變量的 FadingTooltip 對象的指針。一種實(shí)現(xiàn)方式是在 HTML 元素上添加一個(gè) fadingTooltip
屬性,這個(gè)屬性指向 FadingTooltip 對象,然后用它調(diào)用對象的 handleEvent
方法。這樣的話,當(dāng)執(zhí)行 handleEvent
方法時(shí),this
會指向 FadingTooltip 對象。
在 Internet Explorer 中連接鼠標(biāo)事件
Microsoft Internet Explorer 當(dāng)前不支持提議的 W3C 標(biāo)準(zhǔn)事件模型,而是提供它自己的一個(gè)相似的事件模型。它們之間的差異如下:
attachEvent
方法來連接事件,調(diào)用時(shí)要傳遞略有不同的事件類型和函數(shù),見 清單 4。
這是使用函數(shù)閉包在函數(shù)定義中封閉變量的第一種方法,但不是惟一的方法,目前就采用這種方法。
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ù)不會成為這個(gè)元素的方法。當(dāng)鼠標(biāo)事件發(fā)生時(shí),瀏覽器會調(diào)用它們,但是內(nèi)置的 this
變量將指向全局的 window
對象,而不是 HTML 元素,所以函數(shù)不能通過 HTML 元素中保存的指針找到它們的 FadingTooltip 對象。
幸運(yùn)的是,匿名函數(shù)定義位于對象構(gòu)造方法的 htmlElement
參數(shù)的詞法范圍內(nèi)。只需在匿名函數(shù)定義中使用 htmlElement
變量,就可以用這些函數(shù)封閉它。這稱為函數(shù)閉包(function closure):當(dāng)在另一個(gè)函數(shù)內(nèi)定義一個(gè)函數(shù)時(shí),如果內(nèi)部函數(shù)使用外部函數(shù)的局部變量,JavaScript 就會用內(nèi)部函數(shù)的定義保存這些變量。這樣的話,當(dāng)外部函數(shù)返回之后,在調(diào)用內(nèi)部函數(shù)時(shí),外部函數(shù)的局部變量仍然是可用的。
在這里,當(dāng)構(gòu)造方法返回之后,JavaScript 仍然保留 htmlElement
變量的值,所以當(dāng)瀏覽器調(diào)用匿名函數(shù)時(shí),匿名函數(shù)仍然可以使用這個(gè)變量。這使它們能夠找到它們的 HTML 元素并通過指針引用它們的 FadingTooltip 對象,而不需要瀏覽器的幫助。
因?yàn)楹瘮?shù)閉包是 JavaScript 語言的一項(xiàng)特性,所以它們在使用 W3C 事件模型的瀏覽器中一樣是有效的??梢岳眠@個(gè)特性將構(gòu)造方法的 htmlElement
參數(shù)值封閉在前一節(jié)定義的匿名函數(shù)中,而不使用內(nèi)置的 this
變量。
對于既不支持 W3C 事件模型,也不支持 Internet Explorer 事件模型的老式瀏覽器,必須使用 Netscape Navigator 早期版本提供的原始事件模型來連接事件。所有流行的瀏覽器都支持它,而且 Web 設(shè)計(jì)人員廣泛使用它在 Web 頁面上建立動畫;但是對于實(shí)現(xiàn)更復(fù)雜的應(yīng)用程序,這是最后的選擇,因?yàn)樗荒苕溄佣鄠€(gè)事件處理器。為此,需要將以前注冊的事件函數(shù)的指針封閉在自己的事件函數(shù)定義中,然后在調(diào)用自己的 handleEvent
方法之后調(diào)用它們,見清單 5。
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àn)殒溄拥闹羔槍τ谒鼈儾豢稍L問。
為了適應(yīng)性更強(qiáng),代碼將構(gòu)造方法的 this
變量(它指向 FadingTooltip 對象)復(fù)制到局部變量 self
中,然后使用 self
指針在匿名函數(shù)定義中定位 FadingTooltip 對象。這就把 FadingTooltip 對象的指針封閉在匿名函數(shù)定義中,所以當(dāng)任何瀏覽器調(diào)用它們時(shí),它們都可以直接定位 FadingTooltip 對象,而不依靠瀏覽器提供 HTML 元素的指針,也不需要將 FadingTooltip 對象的指針存儲在 HTML 元素中。
對于為 W3C 和 Microsoft 事件模型定義的匿名函數(shù),都可以將 FadingTooltip 對象的指針封閉在其中。這樣就不必將對象的指針保存在 HTML 元素中,并可以在所有事件模型中應(yīng)用同樣的 HTML 元素定位技術(shù)。源代碼 中的構(gòu)造方法就采用這種方法。
既然已經(jīng)連接了所有流行的瀏覽器中的鼠標(biāo)事件,對象構(gòu)造方法已經(jīng)完整了,可以返回到對象原型了。
![]() ![]() |
![]()
|
設(shè)置計(jì)時(shí)器并連接計(jì)時(shí)器事件
我們已經(jīng)完成了 FadingTooltip 構(gòu)造方法,可以繼續(xù)填充它的原型。在 JavaScript 中,對象原型可以包含方法和變量;方法僅僅是指向函數(shù)的變量。首先定義一些通用的方法,它們啟動和取消計(jì)時(shí)器。
在第 1 部分中的設(shè)計(jì)階段提到過,JavaScript 提供兩種類型的計(jì)時(shí)器:一次定時(shí)器和重復(fù)斷續(xù)器,有限狀態(tài)機(jī)需要這兩種定時(shí)器??梢酝ㄟ^調(diào)用 setTimeout
或 setInterval
函數(shù)啟動計(jì)時(shí)器,傳遞的參數(shù)是一個(gè)時(shí)間值(以毫秒為單位)以及當(dāng)發(fā)生 timeout 或 timetick 事件時(shí)要調(diào)用的函數(shù)。它們返回不透明度的引用,可以將這些引用傳遞給 clearTimeout
或 clearInterval
函數(shù)來取消計(jì)時(shí)器。
當(dāng)超過 timeout
值指定的時(shí)間時(shí),或者在每次到達(dá) timetick
時(shí)間間隔時(shí),瀏覽器將調(diào)用傳遞給 setTimeout
和 setInterval
函數(shù)的計(jì)時(shí)器事件函數(shù)(對于 timetick
,這個(gè)過程一直重復(fù)到取消計(jì)時(shí)器為止)。但是,這些 timeout
和 timetick
函數(shù)不會成為任何對象的方法。當(dāng)瀏覽器調(diào)用它們時(shí),this
變量指向全局的 window 對象。瀏覽器并不將關(guān)于計(jì)時(shí)器事件的任何信息傳遞給這些函數(shù)。
學(xué)會處理 鼠標(biāo)事件 之后,連接計(jì)時(shí)器事件也就不困難了。當(dāng)設(shè)置計(jì)時(shí)器時(shí),將內(nèi)置的 this
變量(它指向包含狀態(tài)變量的 FadingTooltip 對象)復(fù)制到局部變量 self
中。self
變量處于 setTimeout
和 setInterval
函數(shù)調(diào)用的詞法范圍。然后,定義使用 self
變量的匿名函數(shù),并將它們作為參數(shù)傳遞給 setTimeout
和 setInterval
函數(shù)。這將 self
變量封閉在函數(shù)定義中,所以當(dāng)瀏覽器調(diào)用函數(shù)時(shí)它仍然可用,見清單 6。
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); }, ... |
計(jì)時(shí)器事件函數(shù)沒有鼠標(biāo)事件函數(shù)那么復(fù)雜。它們僅僅創(chuàng)建一個(gè)簡單的計(jì)時(shí)器事件對象,其中只包含一種事件類型 —— timeout
或者 timetick
,并將它傳遞給處理鼠標(biāo)事件的同一個(gè) handleEvent
方法。
![]() ![]() |
![]()
|
在 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。
if ( htmlElement.fadingTooltip == htmlElement["fadingTooltip"] ) ... // always true |
因此我們將 狀態(tài)表 實(shí)現(xiàn)為一個(gè)二維的函數(shù)關(guān)聯(lián)數(shù)組。直接使用狀態(tài)名稱和事件名稱作為索引。數(shù)組的非空單元格指向匿名函數(shù),這些匿名函數(shù)通過調(diào)用實(shí)用程序方法(比如啟動和取消 計(jì)時(shí)器 的函數(shù))來為事件執(zhí)行操作,然后返回下一個(gè)狀態(tài)。handleEvent
方法的代碼將使用數(shù)組語法調(diào)用這些操作/轉(zhuǎn)換函數(shù),如清單 8 中的代碼所示。
var nextState = this.actionTransitionFunctions[this.currentState][event.type](event); |
handleEvent
方法以關(guān)聯(lián)數(shù)組的形式訪問 actionTransitionFunctions
表,使用當(dāng)前狀態(tài)和事件類型作為索引,并選擇要調(diào)用的函數(shù)。它將事件對象作為參數(shù)傳遞給這個(gè)函數(shù)。這個(gè)函數(shù)將執(zhí)行所需的操作,然后返回下一個(gè)狀態(tài)的名稱。
因?yàn)殛P(guān)聯(lián)數(shù)組是對象(反之亦然),所以可以使用對象語法定義 actionTransitionFunctions
表,但是 handleEvent
方法將使用數(shù)組語法訪問它。例如,在 Inactive
的初始狀態(tài)中,可能出現(xiàn)的惟一事件是 mouseover
,所以可以定義一個(gè)處理此情況的函數(shù),見清單 9。
FadingTooltip.prototype = { ... initialState: ‘Inactive‘, actionTransitionFunctions: { Inactive: { mouseover: function(event) { this.cancelTimer(); this.saveCursorPosition(event); this.startTimer(this.pauseTime*1000); return ‘Pause‘; } }, ... |
FadingTooltip 對象的原型包含 actionTransitionFunctions
屬性,其值是另一個(gè)對象。它包含另一個(gè)屬性 Inactive
,其值也是另一個(gè)對象。它只包含一個(gè)屬性 mouseover
,其值是一個(gè)函數(shù)。當(dāng)在 Inactive
狀態(tài)下發(fā)生 mouseover
事件時(shí),handleEvent
方法將調(diào)用這個(gè)函數(shù)。它需要一個(gè)名為 event
的參數(shù),通過調(diào)用三個(gè)實(shí)用程序函數(shù)來執(zhí)行三個(gè)操作,然后返回 Pause
作為下一個(gè)狀態(tài)的名稱。操作包括保存鼠標(biāo)位置(這是瀏覽器存儲在鼠標(biāo)事件對象中的)和啟動計(jì)時(shí)器,其超時(shí)值是一個(gè)名為 pauseTime
的參數(shù)(以秒作為單位,所以按照 startTimer
方法的要求,將它轉(zhuǎn)換為毫秒)。
部件在 Pause
狀態(tài)下需要響應(yīng)三個(gè)事件:mousemove
、mouseout
和 timeout
事件。在 actionTransitionFunctions
表中定義一個(gè) Pause
對象,它具有分別對應(yīng)于這些事件類型的屬性,如清單 10 所示。
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‘; } }, ... |
當(dāng)在 Pause
狀態(tài)下發(fā)生 mousemove
事件時(shí),handleEvent
方法將調(diào)用一個(gè)函數(shù),這個(gè)函數(shù)簡單地調(diào)用 doActionTransition
方法,傳遞 event
參數(shù),并返回它所返回的值。與 handleEvent
方法相似,doActionTransition
方法使用它的前兩個(gè)參數(shù)作為數(shù)組索引訪問 actionTransitionFunctions
表,并將它的第三個(gè)參數(shù)傳遞給在數(shù)組中找到的函數(shù)。當(dāng)發(fā)生 mouseout
事件時(shí),代碼調(diào)用一個(gè)函數(shù),它會取消本節(jié)前面啟動的計(jì)時(shí)器,然后轉(zhuǎn)換回 Inactive
狀態(tài)。
當(dāng)發(fā)生 timeout
事件時(shí),將取消任何正在運(yùn)行的計(jì)時(shí)器,創(chuàng)建一個(gè)初始不透明度為 0 的工具提示,啟動一個(gè)斷續(xù)器,并轉(zhuǎn)換到 FadeIn
狀態(tài)。
與 actionTransitionFunctions
表中的其他函數(shù)一樣,定義一個(gè)在 FadeIn
狀態(tài)下處理 timetick
事件的函數(shù),見清單 11。
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; } }, .... |
每當(dāng)在 FadeIn
狀態(tài)下發(fā)生 timetick
事件時(shí),handleEvent
方法將調(diào)用一個(gè)函數(shù),它略微增加工具提示的不透明度。淡入時(shí)間(以秒為單位指定)、動畫速率(不透明度從 0 開始增加的速度,用每秒的步數(shù)指定)和最大不透明度(指定為 0.0 到 1.0 之間的浮點(diǎn)數(shù))都是參數(shù)。這個(gè)函數(shù)將返回當(dāng)前狀態(tài),讓有限狀態(tài)機(jī)保持在 FadeIn
狀態(tài),直到工具提示的不透明度到達(dá)最大不透明度參數(shù)。然后,它取消斷續(xù)器,啟動一個(gè)計(jì)時(shí)器來顯示工具提示,并轉(zhuǎn)換到 Display
狀態(tài)。
以相似的方式定義 actionTransitionFunctions
表中的其他函數(shù)。細(xì)節(jié)請參考完整的 源代碼(其中有很多注釋),并參照 圖 1。
![]() ![]() |
![]()
|
我們已經(jīng)多次提到 handleEvent
方法,所以它的實(shí)現(xiàn)應(yīng)該并不神秘了,見清單 12。
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
表的實(shí)際實(shí)現(xiàn)與 前一節(jié) 中的建議不太一樣。這個(gè)方法使用當(dāng)前狀態(tài)和事件類型作為關(guān)聯(lián)數(shù)組的索引,從 actionTransitionFunctions
表中選擇要調(diào)用的函數(shù)。但是,這個(gè)方法將所選函數(shù)的指針復(fù)制到一個(gè)局部變量中,然后用 function 對象的 call
方法調(diào)用這個(gè)函數(shù)。而不是直接調(diào)用它。能夠這樣做是因?yàn)?,與其他值一樣,function 對象可以賦值給變量。必須這樣做是因?yàn)?,?dāng)執(zhí)行函數(shù)時(shí),內(nèi)置的 this
變量需要指向 FadingTooltip 對象。如果像前面建議的那樣,使用數(shù)組索引從 actionTransitionFunctions
表直接調(diào)用函數(shù),this
變量就會指向這個(gè)表。function 對象的 call
方法會將 this
設(shè)置為它的第一個(gè)參數(shù),然后調(diào)用函數(shù),傳遞其余的參數(shù)。
請記住,actionTransitionFunctions
表是稀疏的;為每個(gè)狀態(tài)下期望出現(xiàn)的事件定義函數(shù),其他單元格都空著。handleEvent
方法通過調(diào)用 unexpectedEvent
方法來處理任何不期望出現(xiàn)的事件。如果某個(gè)操作/轉(zhuǎn)換函數(shù)返回不屬于有效狀態(tài)的值,它將調(diào)用 undefinedState
方法。這些方法將取消任何正在運(yùn)行的計(jì)時(shí)器,如果已經(jīng)創(chuàng)建了工具提示,就刪除它,并將有限狀態(tài)機(jī)返回到初始狀態(tài)。一個(gè)方法見清單 13;另一個(gè)方法幾乎是相同的。
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; }, ... |
這些方法將顯示一個(gè)描述錯(cuò)誤的警告對話框,希望用戶將錯(cuò)誤描述發(fā)給代碼的作者。
![]() ![]() |
![]()
|
除了工具提示本身之外,所有東西都實(shí)現(xiàn)了,現(xiàn)在不用再等了。
當(dāng)在 Pause
狀態(tài)下出現(xiàn) timeout
事件時(shí),希望工具提示出現(xiàn)在鼠標(biāo)光標(biāo)附近,但是瀏覽器沒有將鼠標(biāo)位置傳遞給計(jì)時(shí)器事件。幸運(yùn)的是,瀏覽器會將鼠標(biāo)位置傳遞給鼠標(biāo)事件,所以當(dāng)發(fā)生鼠標(biāo)事件時(shí),可以調(diào)用 saveCursorPosition
方法將它保存在狀態(tài)變量中,見清單 14。
FadingTooltip.prototype = { ... saveCursorPosition: function(event) { this.lastCursorX = event.clientX; this.lastCursorY = event.clientY; }, ... |
工具提示是一個(gè) HTML div 元素,其中可以包含任何文本、圖像和標(biāo)記,它在 tooltipContent
參數(shù)中傳遞給構(gòu)造方法。createTooltip
方法見清單 15。
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)用默認(rèn)的基本樣式。但是工具提示的幾個(gè)方面依賴于它的外觀,比如它的位置和不透明度,所以要覆蓋與這些屬性相關(guān)的任何樣式,這可以在樣式表中指定。HTML div 元素將用絕對坐標(biāo)定位在頁面上,接近最近保存的鼠標(biāo)位置,在任何重疊的其他元素上面。它的初始不透明度是 0,即完全透明。
每當(dāng)在 FadeIn
或 FadeOut
狀態(tài)下發(fā)生 timetick
事件時(shí),分別調(diào)用 fadeTooltip
方法略微增加或減少工具提示的不透明度,同時(shí)確保不透明度處于 0 和最大不透明度參數(shù)之間,見清單 16。
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ù)也需要移動和刪除工具提示的實(shí)用程序方法。它們的實(shí)現(xiàn)非常簡單明了,可以通過 源代碼文件 中的注釋理解它們。
正如在本文的這一部分中提到的,需要定義參數(shù)才能完成實(shí)現(xiàn)。它們是對象原型的屬性,但是與狀態(tài)變量不同,它們具有清單 17 所示的默認(rèn)值。
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
是一個(gè)用 JavaScript Object Notation(有時(shí)稱為 JSON)編寫的對象,它可以覆蓋這些屬性的默認(rèn)值,見清單 18。
function FadingTooltip(htmlElement, tooltipContent, parameters) { ... for (parameter in parameters) { if (typeof(this[parameter])!=‘undefined‘) this[parameter] = parameters[parameter]; } ... }; |
構(gòu)造方法在它的 parameters
參數(shù)中檢查每個(gè)屬性;對于每個(gè)屬性,如果它存在于原型中,那么它的值覆蓋參數(shù)的默認(rèn)值。請記住,原型是一個(gè)對象,所以它也是一個(gè)關(guān)聯(lián)數(shù)組。這里同樣使用對象表示法定義參數(shù),但是用數(shù)組表示法訪問它們。
現(xiàn)在,F(xiàn)adingTooltip 的實(shí)現(xiàn)已經(jīng)完成了。您可以 下載 構(gòu)造方法和原型的源代碼。
![]() ![]() |
![]()
|
在對實(shí)現(xiàn)進(jìn)行測試之前,要對性能做幾點(diǎn)說明。
瀏覽器同步地執(zhí)行 JavaScript 程序。當(dāng)連接的事件發(fā)生時(shí),瀏覽器調(diào)用它的事件處理器,并等待它返回,然后再繼續(xù)處理下一個(gè)事件。如果在事件處理器返回之前發(fā)生了更多的事件,瀏覽器就將它們放在隊(duì)列中;當(dāng)事件處理器返回時(shí),依次同步地處理排隊(duì)的事件,每次一個(gè)。如果一個(gè)事件處理器花費(fèi)了過長時(shí)間,它可能會延遲瀏覽器本身對未連接的事件的響應(yīng)。用戶就可能認(rèn)為程序反應(yīng)緩慢,或者認(rèn)為瀏覽器出了故障。
所以一定要使事件處理器盡可能簡短,這在用密集的計(jì)時(shí)器事件模擬動畫的程序中尤其重要。如果 timetick
事件處理器花費(fèi)的時(shí)間超過了斷續(xù)器的時(shí)間間隔,timetick
事件就會在瀏覽器的事件隊(duì)列中積累起來,導(dǎo)致處理器飽和并使瀏覽器反應(yīng)緩慢。
例如,假設(shè)動畫的默認(rèn)速率是每秒 24 步,一個(gè) timetick
事件處理器在返回到瀏覽器之前,有差不多 40 毫秒時(shí)間完成它需要的操作(假設(shè)它占用全部處理器時(shí)間)。在現(xiàn)代的工作站上,這段時(shí)間足夠進(jìn)行許多處理。但是,我們的目標(biāo)不是在這段時(shí)間內(nèi)做盡可能多的工作,而是使用盡可能少的處理器時(shí)間。如果程序?qū)崿F(xiàn)的處理器使用率非常低,那么即使在其他活動的負(fù)載很高的處理器上,動畫效果也會平滑地運(yùn)行,程序能夠做出正常的響應(yīng)。
不要將動畫速率設(shè)置為每秒 60 或 85 步(因?yàn)槟J(rèn)為動畫速率與顯示器的刷新頻率匹配會產(chǎn)生更平滑的動畫)。這會將 timetick
事件之間的時(shí)間減少到大約 12 毫秒。如果 timetick
事件處理器花費(fèi)的時(shí)間超過這個(gè)值,或者有其他活動爭奪處理器,那么動畫可能變得不平滑,或者瀏覽器變得響應(yīng)緩慢。
![]() ![]() |
![]()
|
完成了實(shí)現(xiàn)之后,就要在一些瀏覽器中對代碼進(jìn)行測試了。這是本系列第 3 部分的主題。不過請記住,開發(fā)是個(gè)反復(fù)的過程,有時(shí)可能需要返回設(shè)計(jì)或?qū)崿F(xiàn)階段...