深入理解requestAnimationFrame - 碼農終結者

前言

本文主要參考w3c資料,從底層實現原理的角度介紹了requestAnimationFrame、cancelAnimationFrame,給出了相關的示例代碼以及我對實現原理的理解和討論。


本文介紹

瀏覽器中動畫有兩種實現形式:通過申明元素實現(如SVG中的

元素)和腳本實現。

可以通過setTimeout和setInterval方法來在腳本中實現動畫,但是這樣效果可能不夠流暢,且會占用額外的資源。可參考《Html5 Canvas核心技術》中的論述:

它們有如下的特征:

1、即使向其傳遞毫秒為單位的參數,它們也不能達到ms的準確性。這是因為javascript是單線程的,可能會發生阻塞。

2、沒有對調用動畫的循環機制進行優化。

3、沒有考慮到繪制動畫的最佳時機,只是一味地以某個大致的事件間隔來調用循環。

其實,使用setInterval或setTimeout來實現主循環,根本錯誤就在于它們抽象等級不符合要求。我們想讓瀏覽器執行的是一套可以控制各種細節的api,實現如“最優幀速率”、“選擇繪制下一幀的最佳時機”等功能。但是如果使用它們的話,這些具體的細節就必須由開發者自己來完成。

requestAnimationFrame不需要使用者指定循環間隔時間,瀏覽器會基于當前頁面是否可見、CPU的負荷情況等來自行決定最佳的幀速率,從而更合理地使用CPU。


名詞說明

動畫幀請求回調函數列表

每個Document都有一個動畫幀請求回調函數列表,該列表可以看成是由< handle, callback>元組組成的集合。其中handle是一個整數,唯一地標識了元組在列表中的位置;callback是一個無返回值的、形參為一個時間值的函數(該時間值為由瀏覽器傳入的從1970年1月1日到當前所經過的毫秒數)。 剛開始該列表為空。

Document

Dom模型中定義的Document節點。

Active document

瀏覽器上下文browsingContext中的Document被指定為active document。

browsingContext

瀏覽器上下文。

瀏覽器上下文是呈現document對象給用戶的環境。 瀏覽器中的1個tab或一個窗口包含一個頂級瀏覽器上下文,如果該頁面有iframe,則iframe中也會有自己的瀏覽器上下文,稱為嵌套的瀏覽器上下文。

DOM模型

詳見我的理解DOM。

document對象

當html文檔加載完成后,瀏覽器會創建一個document對象。它對應于Document節點,實現了HTML的Document接口。 通過該對象可獲得整個html文檔的信息,從而對HTML頁面中的所有元素進行訪問和操作。

HTML的Document接口

該接口對DOM定義的Document接口進行了擴展,定義了 HTML 專用的屬性和方法。

詳見The Document object

頁面可見

當頁面被最小化或者被切換成后臺標簽頁時,頁面為不可見,瀏覽器會觸發一個 visibilitychange事件,并設置document.hidden屬性為true;切換到顯示狀態時,頁面為可見,也同樣觸發一個 visibilitychange事件,設置document.hidden屬性為false。

詳見Page VisibilityPage Visibility(頁面可見性) API介紹、微拓展

隊列

瀏覽器讓一個單線程共用于執行javascrip和更新用戶界面。這個線程通常被稱為“瀏覽器UI線程”。 瀏覽器UI線程的工作基于一個簡單的隊列系統,任務會被保存到隊列中直到進程空閑。一旦空閑,隊列中的下一個任務就被重新提取出來并運行。這些任務要么是運行javascript代碼,要么執行UI更新,包括重繪和重排。

API接口

Window對象定義了以下兩個接口:

partial interface Window {

long requestAnimationFrame(FrameRequestCallback callback);

void cancelAnimationFrame(long handle);

};


requestAnimationFrame

requestAnimationFrame方法用于通知瀏覽器重采樣動畫。

當requestAnimationFrame(callback)被調用時不會執行callback,而是會將元組< handle,callback>插入到動畫幀請求回調函數列表末尾(其中元組的callback就是傳入requestAnimationFrame的回調函數),并且返回handle值,該值為瀏覽器定義的、大于0的整數,唯一標識了該回調函數在列表中位置。

每個回調函數都有一個布爾標識cancelled,該標識初始值為false,并且對外不可見。

在后面的“處理模型” 中我們會看到,瀏覽器在執行“采樣所有動畫”的任務時會遍歷動畫幀請求回調函數列表,判斷每個元組的callback的cancelled,如果為false,則執行callback。

cancelAnimationFrame

cancelAnimationFrame 方法用于取消先前安排的一個動畫幀更新的請求。

當調用cancelAnimationFrame(handle)時,瀏覽器會設置該handle指向的回調函數的cancelled為true。

無論該回調函數是否在動畫幀請求回調函數列表中,它的cancelled都會被設置為true。

如果該handle沒有指向任何回調函數,則調用cancelAnimationFrame 不會發生任何事情。

處理模型

當頁面可見并且動畫幀請求回調函數列表不為空時,瀏覽器會定期地加入一個“采樣所有動畫”的任務到UI線程的隊列中。

此處使用偽代碼來說明“采樣所有動畫”任務的執行步驟:

var list = {};

var browsingContexts = 瀏覽器頂級上下文及其下屬的瀏覽器上下文;

for (var browsingContext in browsingContexts) {

var time = 從1970年1月1日到當前所經過的毫秒數;

var d = browsingContext的active document;? //即當前瀏覽器上下文中的Document節點

//如果該active document可見

if (d.hidden !== true) {

//拷貝active document的動畫幀請求回調函數列表到list中,并清空該列表

var doclist = d的動畫幀請求回調函數列表

doclist.appendTo(list);

clear(doclist);

}

//遍歷動畫幀請求回調函數列表的元組中的回調函數

for (var callback in list) {

if (callback.cancelled !== true) {

try {

//每個browsingContext都有一個對應的WindowProxy對象,WindowProxy對象會將callback指向active document關聯的window對象。

//傳入時間值time

callback.call(window, time);

}

//忽略異常

catch (e) {

}

}

}

}

已解決的問題

為什么在callback內部執行cancelAnimationFrame不能取消動畫?

問題描述

如下面的代碼會一直執行a:

var id = null;

function a(time) {

console.log("animation");

window.cancelAnimationFrame(id); //不起作用

id = window.requestAnimationFrame(a);

}

a();

原因分析

我們來分析下這段代碼是如何執行的:

1、執行a

(1)執行“a();”,執行函數a;

(2)執行“console.log("animation");”,打印“animation”;

(3)執行“window.cancelAnimationFrame(id);”,因為id為null,瀏覽器在動畫幀請求回調函數列表中找不到對應的callback,所以不發生任何事情;

(4)執行“id = window.requestAnimationFrame(a);”,瀏覽器會將一個元組< handle, a>插入到Document的動畫幀請求回調函數列表末尾,將id賦值為該元組的handle值;

2、a執行完畢后,執行第一個“采樣所有動畫”的任務

假設當前頁面一直可見,因為動畫幀請求回調函數列表不為空,所以瀏覽器會定期地加入一個“采樣所有動畫”的任務到線程隊列中。

a執行完畢后的第一個“采樣所有動畫”的任務執行時會進行以下步驟:

(1)拷貝Document的動畫幀請求回調函數列表到list變量中,清空Document的動畫幀請求回調函數列表;

(2)遍歷list的列表,列表有1個元組,該元組的callback為a;

(3)判斷a的cancelled,為默認值false,所以執行a;

(4)執行“console.log("animation");”,打印“animation”;

(5)執行“window.cancelAnimationFrame(id);”,此時id指向當前元組的a(即當前正在執行的a),瀏覽器將

當前元組

的a的cancelled設為true。

(6)執行“id = window.requestAnimationFrame(a);”,瀏覽器會將

新的元組< handle, a>

插入到Document的動畫幀請求回調函數列表末尾(新元組的a的cancelled為默認值false),將id賦值為該元組的handle值。

3、執行下一個“采樣所有動畫”的任務

當下一個“采樣所有動畫”的任務執行時,會判斷動畫幀請求回調函數列表的元組的a的cancelled,因為該元組為新插入的元組,所以值為默認值false,因此會繼續執行a。

如此類推,瀏覽器會一直循環執行a。

解決方案

有下面兩個方案:

1、執行requestAnimationFrame之后再執行cancelAnimationFrame。

下面代碼只會執行一次a:

var id = null;

function a(time) {

console.log("animation");

id = window.requestAnimationFrame(a);

window.cancelAnimationFrame(id);

}

a();

2、在callback外部執行cancelAnimationFrame。 下面代碼只會執行一次a:

function a(time) {

console.log("animation");

id = window.requestAnimationFrame(a);

}

a();

window.cancelAnimationFrame(id);

因為執行“window.cancelAnimationFrame(id);”時,id指向了新插入到動畫幀請求回調函數列表中的元組的a,所以 “采樣所有動畫”任務判斷元組的a的cancelled時,該值為true,從而不再執行a。

注意事項

1、在處理模型 中我們已經看到,在遍歷執行拷貝的動畫幀請求回調函數列表中的回調函數之前,Document的動畫幀請求回調函數列表已經被清空了。因此如果要多次執行回調函數,需要在回調函數中再次調用requestAnimationFrame將包含回調函數的元組加入到Document的動畫幀請求回調函數列表中,從而瀏覽器才會再次定期加入“采樣所有動畫”的任務(當頁面可見并且動畫幀請求回調函數列表不為空時,瀏覽器才會加入該任務),執行回調函數。

例如下面代碼只執行1次animate函數:

var id = null;

function animate(time) {

console.log("animation");

}

window.requestAnimationFrame(animate);

下面代碼會一直執行animate函數:

var id = null;

function animate(time) {

console.log("animation");

window.requestAnimationFrame(animate);

}

animate();

2、如果在執行回調函數或者Document的動畫幀請求回調函數列表被清空之前多次調用requestAnimationFrame插入同一個回調函數,那么列表中會有多個元組指向該回調函數(它們的handle不同,但callback都為該回調函數),“采集所有動畫”任務會執行多次該回調函數。

例如下面的代碼在執行“id1 = window.requestAnimationFrame(animate);”和“id2 = window.requestAnimationFrame(animate);”時會將兩個元組(handle分別為id1、id2,回調函數callback都為animate)插入到Document的動畫幀請求回調函數列表末尾。 因為“采樣所有動畫”任務會遍歷執行動畫幀請求回調函數列表的每個回調函數,所以在“采樣所有動畫”任務中會執行兩次animate。

//下面代碼會打印兩次"animation"

var id1 = null,

id2 = null;

function animate(time) {

console.log("animation");

}

id1 = window.requestAnimationFrame(animate);

id2 = window.requestAnimationFrame(animate);? //id1和id2值不同,指向列表中不同的元組,這兩個元組中的callback都為同一個animate

兼容性方法

下面為《HTML5 Canvas 核心技術》給出的兼容主流瀏覽器的requestNextAnimationFrame 和cancelNextRequestAnimationFrame方法,大家可直接拿去用:

window.requestNextAnimationFrame = (function () {

var originalWebkitRequestAnimationFrame = undefined,

wrapper = undefined,

callback = undefined,

geckoVersion = 0,

userAgent = navigator.userAgent,

index = 0,

self = this;

// Workaround for Chrome 10 bug where Chrome

// does not pass the time to the animation function

if (window.webkitRequestAnimationFrame) {

// Define the wrapper

wrapper = function (time) {

if (time === undefined) {

time = +new Date();

}

self.callback(time);

};

// Make the switch

originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;

window.webkitRequestAnimationFrame = function (callback, element) {

self.callback = callback;

// Browser calls the wrapper and wrapper calls the callback

originalWebkitRequestAnimationFrame(wrapper, element);

}

}

// Workaround for Gecko 2.0, which has a bug in

// mozRequestAnimationFrame() that restricts animations

// to 30-40 fps.

if (window.mozRequestAnimationFrame) {

// Check the Gecko version. Gecko is used by browsers

// other than Firefox. Gecko 2.0 corresponds to

// Firefox 4.0.

index = userAgent.indexOf('rv:');

if (userAgent.indexOf('Gecko') != -1) {

geckoVersion = userAgent.substr(index + 3, 3);

if (geckoVersion === '2.0') {

// Forces the return statement to fall through

// to the setTimeout() function.

window.mozRequestAnimationFrame = undefined;

}

}

}

return? window.requestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.mozRequestAnimationFrame ||

window.oRequestAnimationFrame ||

window.msRequestAnimationFrame ||

function (callback, element) {

var start,

finish;

window.setTimeout(function () {

start = +new Date();

callback(start);

finish = +new Date();

self.timeout = 1000 / 60 - (finish - start);

}, self.timeout);

};

}());

window.cancelNextRequestAnimationFrame = window.cancelRequestAnimationFrame

|| window.webkitCancelAnimationFrame

|| window.webkitCancelRequestAnimationFrame

|| window.mozCancelRequestAnimationFrame

|| window.oCancelRequestAnimationFrame

|| window.msCancelRequestAnimationFrame

|| clearTimeout;


參考資料

Timing control for script-based animations

Browsing contexts

The Document object

《HTML5 Canvas核心技術》

理解DOM

Page Visibility

Page Visibility(頁面可見性) API介紹、微拓展

HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB BROWSERS

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容