本節書摘來自異步社群《javascript高效圖形程式設計(修訂版)》一書中的第2章,第2.3節,作者:【美】raffaele cecco著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視
本節将讨論如何在javascript中控制圖形的更新速度以保證使用者體驗。我們想要圖形有平滑流暢的移動,不要太快也不要太慢。使用者計算機的性能會影響圖形更新的速度。下面我們将讨論減少不同機器上速度差異的解決方案。
2.3.1 使用setinterval和settimeout
javascript的setinterval()和settimeout()函數使你可以定期調用javascript代碼。需要定期更新圖形的應用,比如電腦遊戲,幾乎都離不開這些函數。
你可以将回調函數傳給setinterval()來重複調用此函數:

注意執行bigfunction()花費20毫秒。如果循環間隔比這個值小呢?
看起來20毫秒的bigfunction()會在第一個回調函數傳回之前被重新調用。實際上,新的回調将排在隊列中直到前面的回調函數結束。
如果延時更短點呢?
可以預計每執行一個回調函數,若幹個回調函數在排隊。事實上,通常的行為是隻有一個排隊的bigfunciton()會激活。排隊的回調函數會在第一個回調函數結束後立即執行嗎?有可能,但不一定。其他時間和浏覽器中運作的代碼可能使得setinterval()回調函數被延時或丢棄。回調函數甚至有可能連續發生(比規定的間隔短),如果javascript發現一個時間視窗,可以清除隊列。
這裡想要說明的是:不能保證回調函數以指定的間隔執行。
settimeout()在指定的延時後調用一個函數,和setinterval()類似,但可預見性更強。
這會在50毫秒後,調用一次bigfunction()。和setinterval()一樣,這個延時僅僅是一個參考。
你可以用settimeout()連續調用一個函數,其行為将比setinterval()更可預見:
每當bigfunction()結束,它設定另一個以自己作為回調函數的settimeout()。
在這個例子中,盡管設定的timeout值比bigfunction()執行時間要短,回調函數隻會在bigfunction()結束後再執行。實際上,執行的頻率和下面使用setinterval()的代碼類似:
2.3.2 定時器精度
windows下的浏覽器隻有粗粒度的定時器。例如windows xp的底層作業系統定時器提供15毫秒精度。這意味着date()、setinterval()和settimeout()等javascript函數不能提供可靠的15毫秒以下的定時。google chrome是例外之一,它将windows切換到一個準确的定時器模式并提供1毫秒的精度。
這裡的要點是一個應用程式不應該依賴低于15毫秒(約1/64秒)的定時器。這個問題嚴重嗎?一般情況下不嚴重。浏覽器中不太可能或不應該運作對時間這麼敏感的應用程式。動畫也許會比預計的慢一點或快一點,遊戲等應用程式中幀率也不是絕對的穩定。如果在一段時間内細緻檢查這些不精确的累加效果,也許可以看到一定的誤差。不過,在通常情況下,比如玩遊戲或看菜單特效時,這些誤差是察覺不到的。
不過在使用date()進行代碼性能分析時要小心。下面的例子中,如果執行的代碼太快結束的話,将得到不準确的結果:
一個更好的解決方案是在較長的時間段(如1秒)内循環執行代碼,然後用期間完成的疊代次數來衡量執行速度。
2.3.3 保持速度一緻
前面的sprite實作,具體來說是移動sprite的代碼,存在一個問題——不同的浏覽器下動畫和移動的速度(即幀率)不一樣。比如2.8ghz的pc、opera或google chrome等浏覽器可以在移動100個sprite時輕松達到50fps(每秒幀數),firfox也許能有30fps,而ie8也許隻有25fps。如果考慮不同的硬體,幀率的差異會更大。
這對裝飾性的動畫和特效不是大問題,但遊戲等應用程式需要一緻的移動速度來保證可玩性。
為在不同的軟硬體環境中保持速度一緻,必須在sprite移動和動畫涉及的計算中考慮幀率的不同。具體來說,一個以30fps每幀移動兩個像素的sprite,和一個以60fps每幀移動一個像素的sprite看起來速度一樣。這兩者之間的主要視覺差別是30fps sprite的移動不如60fps sprite的移動流暢。不過至少看起來他們是以同樣的速度在螢幕上移動。
為此,必須計算一個時間系數,并在移動和動畫代碼中使用。表2-4顯示了一個例子。
很明顯,時間系數=目标fps/實際fps。
為計算實際fps,可以用javascript的date對象記錄目前時間(即開始時間,機關為毫秒位)。在執行所有應用邏輯後再記錄時間(結束時間)。下面是代碼:
如果cpu負荷過重,幀率會很慢。以6fps、每幀10像素在螢幕上移動的sprite看起來不平穩,肯定不适合遊戲。表2-5列出了幀率和流暢度的對應關系。
這不是說10fps的低幀率毫無用處。對俄羅斯方塊這樣的遊戲而言,這種幀率也許就可以接受了。
現在我們建立一個timeinfo對象,它将提供保持應用速度一緻所需的所有功能。它接受一個goalfps參數,即我們想要達到的目标fps。如果達不到,函數将調整移動速度使其至少看起來達到了goalfps。timeinfo對象中還提供了其他時間相關的資訊。
下面的函數傳回一個對象,其中包含getinfo()方法。getinfo()方法傳回一個對象,其屬性如表2-6所示。
paused變量表明這是在應用程式開始或暫停後,getinfo()第一次被調用。它保證在經過一個很長的暫停之後,getinfo()傳回的值是良性的,并且不會傳回一個非常大的值。
我們通過從上一次getinfo()中記錄的oldtime,減去新時間,得到經過時間(elapsed time)。然後用經過時間來計算幀率。+new date()語句等價于new date(). gettime();:
然後傳回一些有用的資訊屬性,如表2-6所示。
接着我們定義pause()方法,在暫停應用程式時都應調用此方法。
現在,我們可以在原始的bouncysprite和bouncyboss代碼中使用timeinfo對象了:
moveanddraw方法現在接受時間系數作為參數。計算和原來相似,但使用了時間系數。changeimage()函數的參數應該是整數,但因為animindex受時間系數影響,也許不是整數。為此,我們複制一個整數版的animindex為animindx2,并傳入changeimage():
bouncyboss對象現在要建立一個目标fps為40的timeinfo執行個體(存在timer變量中)。moveall()在每個疊代調用timeinfo.getinfo()得到時間系數,并将其傳給每個bouncysprite執行個體的moveanddraw()方法。注意隻需要一個timeinfo執行個體即可,因為每個bouncysprite執行個體可以使用同一個系數。