前言
本文主要參考w3c資料,從底層實(shí)現(xiàn)原理的角度介紹了requestAnimationFrame、cancelAnimationFrame,給出了相關(guān)的示例代碼以及我對實(shí)現(xiàn)原理的理解和討論。
本文介紹
瀏覽器中動畫有兩種實(shí)現(xiàn)形式:通過申明元素實(shí)現(xiàn)(如SVG中的
元素)和腳本實(shí)現(xiàn)。
可以通過setTimeout和setInterval方法來在腳本中實(shí)現(xiàn)動畫,但是這樣效果可能不夠流暢,且會占用額外的資源??蓞⒖肌禜tml5 Canvas核心技術(shù)》中的論述:
它們有如下的特征:
1、即使向其傳遞毫秒為單位的參數(shù),它們也不能達(dá)到ms的準(zhǔn)確性。這是因?yàn)閖avascript是單線程的,可能會發(fā)生阻塞。
2、沒有對調(diào)用動畫的循環(huán)機(jī)制進(jìn)行優(yōu)化。
3、沒有考慮到繪制動畫的最佳時(shí)機(jī),只是一味地以某個(gè)大致的事件間隔來調(diào)用循環(huán)。
其實(shí),使用setInterval或setTimeout來實(shí)現(xiàn)主循環(huán),根本錯(cuò)誤就在于它們抽象等級不符合要求。我們想讓瀏覽器執(zhí)行的是一套可以控制各種細(xì)節(jié)的api,實(shí)現(xiàn)如“最優(yōu)幀速率”、“選擇繪制下一幀的最佳時(shí)機(jī)”等功能。但是如果使用它們的話,這些具體的細(xì)節(jié)就必須由開發(fā)者自己來完成。
requestAnimationFrame不需要使用者指定循環(huán)間隔時(shí)間,瀏覽器會基于當(dāng)前頁面是否可見、CPU的負(fù)荷情況等來自行決定最佳的幀速率,從而更合理地使用CPU。
名詞說明
動畫幀請求回調(diào)函數(shù)列表
每個(gè)Document都有一個(gè)動畫幀請求回調(diào)函數(shù)列表,該列表可以看成是由< handle, callback>元組組成的集合。其中handle是一個(gè)整數(shù),唯一地標(biāo)識了元組在列表中的位置;callback是一個(gè)無返回值的、形參為一個(gè)時(shí)間值的函數(shù)(該時(shí)間值為由瀏覽器傳入的從1970年1月1日到當(dāng)前所經(jīng)過的毫秒數(shù))。 剛開始該列表為空。
Document
Dom模型中定義的Document節(jié)點(diǎn)。
Active document
瀏覽器上下文browsingContext中的Document被指定為active document。
browsingContext
瀏覽器上下文。
瀏覽器上下文是呈現(xiàn)document對象給用戶的環(huán)境。 瀏覽器中的1個(gè)tab或一個(gè)窗口包含一個(gè)頂級瀏覽器上下文,如果該頁面有iframe,則iframe中也會有自己的瀏覽器上下文,稱為嵌套的瀏覽器上下文。
DOM模型
詳見我的理解DOM。
document對象
當(dāng)html文檔加載完成后,瀏覽器會創(chuàng)建一個(gè)document對象。它對應(yīng)于Document節(jié)點(diǎn),實(shí)現(xiàn)了HTML的Document接口。 通過該對象可獲得整個(gè)html文檔的信息,從而對HTML頁面中的所有元素進(jìn)行訪問和操作。
HTML的Document接口
該接口對DOM定義的Document接口進(jìn)行了擴(kuò)展,定義了 HTML 專用的屬性和方法。
頁面可見
當(dāng)頁面被最小化或者被切換成后臺標(biāo)簽頁時(shí),頁面為不可見,瀏覽器會觸發(fā)一個(gè) visibilitychange事件,并設(shè)置document.hidden屬性為true;切換到顯示狀態(tài)時(shí),頁面為可見,也同樣觸發(fā)一個(gè) visibilitychange事件,設(shè)置document.hidden屬性為false。
詳見Page Visibility、Page Visibility(頁面可見性) API介紹、微拓展
隊(duì)列
瀏覽器讓一個(gè)單線程共用于執(zhí)行javascrip和更新用戶界面。這個(gè)線程通常被稱為“瀏覽器UI線程”。 瀏覽器UI線程的工作基于一個(gè)簡單的隊(duì)列系統(tǒng),任務(wù)會被保存到隊(duì)列中直到進(jìn)程空閑。一旦空閑,隊(duì)列中的下一個(gè)任務(wù)就被重新提取出來并運(yùn)行。這些任務(wù)要么是運(yùn)行javascript代碼,要么執(zhí)行UI更新,包括重繪和重排。
API接口
Window對象定義了以下兩個(gè)接口:
partial interface Window {
long requestAnimationFrame(FrameRequestCallback callback);
void cancelAnimationFrame(long handle);
};
requestAnimationFrame
requestAnimationFrame方法用于通知瀏覽器重采樣動畫。
當(dāng)requestAnimationFrame(callback)被調(diào)用時(shí)不會執(zhí)行callback,而是會將元組< handle,callback>插入到動畫幀請求回調(diào)函數(shù)列表末尾(其中元組的callback就是傳入requestAnimationFrame的回調(diào)函數(shù)),并且返回handle值,該值為瀏覽器定義的、大于0的整數(shù),唯一標(biāo)識了該回調(diào)函數(shù)在列表中位置。
每個(gè)回調(diào)函數(shù)都有一個(gè)布爾標(biāo)識cancelled,該標(biāo)識初始值為false,并且對外不可見。
在后面的“處理模型” 中我們會看到,瀏覽器在執(zhí)行“采樣所有動畫”的任務(wù)時(shí)會遍歷動畫幀請求回調(diào)函數(shù)列表,判斷每個(gè)元組的callback的cancelled,如果為false,則執(zhí)行callback。
cancelAnimationFrame
cancelAnimationFrame 方法用于取消先前安排的一個(gè)動畫幀更新的請求。
當(dāng)調(diào)用cancelAnimationFrame(handle)時(shí),瀏覽器會設(shè)置該handle指向的回調(diào)函數(shù)的cancelled為true。
無論該回調(diào)函數(shù)是否在動畫幀請求回調(diào)函數(shù)列表中,它的cancelled都會被設(shè)置為true。
如果該handle沒有指向任何回調(diào)函數(shù),則調(diào)用cancelAnimationFrame 不會發(fā)生任何事情。
處理模型
當(dāng)頁面可見并且動畫幀請求回調(diào)函數(shù)列表不為空時(shí),瀏覽器會定期地加入一個(gè)“采樣所有動畫”的任務(wù)到UI線程的隊(duì)列中。
此處使用偽代碼來說明“采樣所有動畫”任務(wù)的執(zhí)行步驟:
var list = {};
var browsingContexts = 瀏覽器頂級上下文及其下屬的瀏覽器上下文;
for (var browsingContext in browsingContexts) {
var time = 從1970年1月1日到當(dāng)前所經(jīng)過的毫秒數(shù);
var d = browsingContext的active document;? //即當(dāng)前瀏覽器上下文中的Document節(jié)點(diǎn)
//如果該active document可見
if (d.hidden !== true) {
//拷貝active document的動畫幀請求回調(diào)函數(shù)列表到list中,并清空該列表
var doclist = d的動畫幀請求回調(diào)函數(shù)列表
doclist.appendTo(list);
clear(doclist);
}
//遍歷動畫幀請求回調(diào)函數(shù)列表的元組中的回調(diào)函數(shù)
for (var callback in list) {
if (callback.cancelled !== true) {
try {
//每個(gè)browsingContext都有一個(gè)對應(yīng)的WindowProxy對象,WindowProxy對象會將callback指向active document關(guān)聯(lián)的window對象。
//傳入時(shí)間值time
callback.call(window, time);
}
//忽略異常
catch (e) {
}
}
}
}
已解決的問題
為什么在callback內(nèi)部執(zhí)行cancelAnimationFrame不能取消動畫?
問題描述
如下面的代碼會一直執(zhí)行a:
var id = null;
function a(time) {
console.log("animation");
window.cancelAnimationFrame(id); //不起作用
id = window.requestAnimationFrame(a);
}
a();
原因分析
我們來分析下這段代碼是如何執(zhí)行的:
1、執(zhí)行a
(1)執(zhí)行“a();”,執(zhí)行函數(shù)a;
(2)執(zhí)行“console.log("animation");”,打印“animation”;
(3)執(zhí)行“window.cancelAnimationFrame(id);”,因?yàn)閕d為null,瀏覽器在動畫幀請求回調(diào)函數(shù)列表中找不到對應(yīng)的callback,所以不發(fā)生任何事情;
(4)執(zhí)行“id = window.requestAnimationFrame(a);”,瀏覽器會將一個(gè)元組< handle, a>插入到Document的動畫幀請求回調(diào)函數(shù)列表末尾,將id賦值為該元組的handle值;
2、a執(zhí)行完畢后,執(zhí)行第一個(gè)“采樣所有動畫”的任務(wù)
假設(shè)當(dāng)前頁面一直可見,因?yàn)閯赢嫀埱蠡卣{(diào)函數(shù)列表不為空,所以瀏覽器會定期地加入一個(gè)“采樣所有動畫”的任務(wù)到線程隊(duì)列中。
a執(zhí)行完畢后的第一個(gè)“采樣所有動畫”的任務(wù)執(zhí)行時(shí)會進(jìn)行以下步驟:
(1)拷貝Document的動畫幀請求回調(diào)函數(shù)列表到list變量中,清空Document的動畫幀請求回調(diào)函數(shù)列表;
(2)遍歷list的列表,列表有1個(gè)元組,該元組的callback為a;
(3)判斷a的cancelled,為默認(rèn)值false,所以執(zhí)行a;
(4)執(zhí)行“console.log("animation");”,打印“animation”;
(5)執(zhí)行“window.cancelAnimationFrame(id);”,此時(shí)id指向當(dāng)前元組的a(即當(dāng)前正在執(zhí)行的a),瀏覽器將
當(dāng)前元組
的a的cancelled設(shè)為true。
(6)執(zhí)行“id = window.requestAnimationFrame(a);”,瀏覽器會將
新的元組< handle, a>
插入到Document的動畫幀請求回調(diào)函數(shù)列表末尾(新元組的a的cancelled為默認(rèn)值false),將id賦值為該元組的handle值。
3、執(zhí)行下一個(gè)“采樣所有動畫”的任務(wù)
當(dāng)下一個(gè)“采樣所有動畫”的任務(wù)執(zhí)行時(shí),會判斷動畫幀請求回調(diào)函數(shù)列表的元組的a的cancelled,因?yàn)樵撛M為新插入的元組,所以值為默認(rèn)值false,因此會繼續(xù)執(zhí)行a。
如此類推,瀏覽器會一直循環(huán)執(zhí)行a。
解決方案
有下面兩個(gè)方案:
1、執(zhí)行requestAnimationFrame之后再執(zhí)行cancelAnimationFrame。
下面代碼只會執(zhí)行一次a:
var id = null;
function a(time) {
console.log("animation");
id = window.requestAnimationFrame(a);
window.cancelAnimationFrame(id);
}
a();
2、在callback外部執(zhí)行cancelAnimationFrame。 下面代碼只會執(zhí)行一次a:
function a(time) {
console.log("animation");
id = window.requestAnimationFrame(a);
}
a();
window.cancelAnimationFrame(id);
因?yàn)閳?zhí)行“window.cancelAnimationFrame(id);”時(shí),id指向了新插入到動畫幀請求回調(diào)函數(shù)列表中的元組的a,所以 “采樣所有動畫”任務(wù)判斷元組的a的cancelled時(shí),該值為true,從而不再執(zhí)行a。
注意事項(xiàng)
1、在處理模型 中我們已經(jīng)看到,在遍歷執(zhí)行拷貝的動畫幀請求回調(diào)函數(shù)列表中的回調(diào)函數(shù)之前,Document的動畫幀請求回調(diào)函數(shù)列表已經(jīng)被清空了。因此如果要多次執(zhí)行回調(diào)函數(shù),需要在回調(diào)函數(shù)中再次調(diào)用requestAnimationFrame將包含回調(diào)函數(shù)的元組加入到Document的動畫幀請求回調(diào)函數(shù)列表中,從而瀏覽器才會再次定期加入“采樣所有動畫”的任務(wù)(當(dāng)頁面可見并且動畫幀請求回調(diào)函數(shù)列表不為空時(shí),瀏覽器才會加入該任務(wù)),執(zhí)行回調(diào)函數(shù)。
例如下面代碼只執(zhí)行1次animate函數(shù):
var id = null;
function animate(time) {
console.log("animation");
}
window.requestAnimationFrame(animate);
下面代碼會一直執(zhí)行animate函數(shù):
var id = null;
function animate(time) {
console.log("animation");
window.requestAnimationFrame(animate);
}
animate();
2、如果在執(zhí)行回調(diào)函數(shù)或者Document的動畫幀請求回調(diào)函數(shù)列表被清空之前多次調(diào)用requestAnimationFrame插入同一個(gè)回調(diào)函數(shù),那么列表中會有多個(gè)元組指向該回調(diào)函數(shù)(它們的handle不同,但callback都為該回調(diào)函數(shù)),“采集所有動畫”任務(wù)會執(zhí)行多次該回調(diào)函數(shù)。
例如下面的代碼在執(zhí)行“id1 = window.requestAnimationFrame(animate);”和“id2 = window.requestAnimationFrame(animate);”時(shí)會將兩個(gè)元組(handle分別為id1、id2,回調(diào)函數(shù)callback都為animate)插入到Document的動畫幀請求回調(diào)函數(shù)列表末尾。 因?yàn)椤安蓸铀袆赢嫛比蝿?wù)會遍歷執(zhí)行動畫幀請求回調(diào)函數(shù)列表的每個(gè)回調(diào)函數(shù),所以在“采樣所有動畫”任務(wù)中會執(zhí)行兩次animate。
//下面代碼會打印兩次"animation"
var id1 = null,
id2 = null;
function animate(time) {
console.log("animation");
}
id1 = window.requestAnimationFrame(animate);
id2 = window.requestAnimationFrame(animate);? //id1和id2值不同,指向列表中不同的元組,這兩個(gè)元組中的callback都為同一個(gè)animate
兼容性方法
下面為《HTML5 Canvas 核心技術(shù)》給出的兼容主流瀏覽器的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
《HTML5 Canvas核心技術(shù)》