事件環(huán)
來源 Jake Archibold的事件輪詢演講視頻
在剛開學(xué)使用javascript制作逐幀動(dòng)畫的時(shí)候使用的是setTimeout和setInterval這兩個(gè)api來繪制動(dòng)畫幀钉赁。由于setInterval添加的事件隊(duì)列會(huì)由于任務(wù)執(zhí)行時(shí)間過長(zhǎng)而導(dǎo)致隊(duì)列添加出現(xiàn)錯(cuò)誤衫生,所以一般我都是用setTimeout來調(diào)用。代碼很簡(jiǎn)單:
function animateTimeout() {
animateFunc();
setTimeout(animateTimeout)
}
只需要采用這樣的一種調(diào)用方式赋秀,就能讓你定義的animateFunc
動(dòng)作方法能夠?qū)崿F(xiàn)緩動(dòng)播放的效果奴潘。起初我并未發(fā)現(xiàn)這樣有何不妥,直到我使用了requestAnimationFrame這個(gè)api來實(shí)現(xiàn)了同樣的緩動(dòng)動(dòng)畫之后我就發(fā)現(xiàn)了差別竞穷。先看一下下面這段代碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#divContainer {
width: 300px;
height: 300px;
background-color: red;
}
#divContainer2 {
width: 300px;
height: 300px;
background-color: yellow;
}
</style>
</head>
<body>
<div id="divContainer">timeoutDiv</div>
<div id="divContainer2">animateDiv</div>
<script>
let marginGap = 0;
let marginGap2 = 0;
function animateTimeout() {
divContainer.style.marginLeft = `${++marginGap}px`;
setTimeout(animateTimeout, 0);
}
function animateReq(req) {
divContainer2.style.marginLeft = `${++marginGap2}px`;
requestAnimationFrame(animateReq);
}
animateTimeout();
animateReq();
</script>
</body>
</html>
這里定義了divContainer
和divContainer2
兩個(gè)盒子贯吓,分別使用setTimeout
和requestAnimationFrame
來定時(shí)改變他們的左邊距,達(dá)到一個(gè)平移的動(dòng)畫效果弓柱。然而我卻發(fā)現(xiàn)使用setTimeout
的移動(dòng)速度明顯比requestAnimationFrame
快很多
可以在animateTimeout()
和animateReq()
打印日志看看這兩個(gè)方法的執(zhí)行頻率:
function animateTimeout() {
console.log('logTimeout');
divContainer.style.marginLeft = `${++marginGap}px`;
setTimeout(animateTimeout, 0);
}
function animateReq(req) {
console.log('logRequest');
divContainer2.style.marginLeft = `${++marginGap2}px`;
requestAnimationFrame(animateReq);
}
可以看到animateReq()
執(zhí)行一次沟堡,animateTimeout()
差不多會(huì)執(zhí)行3次疮鲫。所以會(huì)看到animateTimeout()
的速度比animateReq()
快了差不多3倍。那為什么會(huì)這樣呢弦叶?于是我先查詢了一下requestAnimationFrame
的文檔俊犯,發(fā)現(xiàn)這樣一段說明文字
This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation
依據(jù)文檔所說,瀏覽器會(huì)在下次重繪之前調(diào)用requestAnimationFrame
里面的回調(diào)方法伤哺,并且回調(diào)執(zhí)行的頻率是每秒60次燕侠,但是通常會(huì)按照W3C推薦的標(biāo)準(zhǔn)適配顯示器的刷新頻率。也就是說按照大多數(shù)顯示器每秒60次的刷新頻率來確定動(dòng)畫幀的時(shí)間是最合適的立莉,這個(gè)時(shí)間段做出來的動(dòng)畫看起來是最平滑的绢彤。所以上面的例子中的setTimeout
在調(diào)用的時(shí)候,延遲時(shí)間的參數(shù)0ms蜓耻,那么一秒鐘就會(huì)添加1000個(gè)動(dòng)畫函數(shù)的任務(wù)茫舶,頁面會(huì)渲染1000次。但是這顯然超過了顯示器的每秒60次刷新頻率刹淌,所以就會(huì)將這1000次的渲染任務(wù)適配到60次饶氏,那么每次渲染就會(huì)執(zhí)行多次任務(wù)。要想使用setTimeout
達(dá)到和顯示器刷新頻率相同的渲染那么在設(shè)置任務(wù)時(shí)間間隔的時(shí)候就要使用1000 / 60有勾,大概是16.7ms渲染一幀的動(dòng)畫
function animateTimeout() {
divContainer.style.marginLeft = `${++marginGap}px`;
setTimeout(animateTimeout, 1000 / 60);
}
可以看到這下animateTimeout()
和animateReq()
的移動(dòng)速度就差不多了疹启。但是移動(dòng)一段時(shí)間之后animateTimeout()
還是跑到了animateReq()
前面。使用setTimeout
和setInterval
并不是那么精確蔼卡。因?yàn)樗鼈冊(cè)O(shè)置的時(shí)間間隔是將動(dòng)畫回調(diào)在指定的時(shí)間后添加到任務(wù)隊(duì)列中喊崖,并不代表它會(huì)到時(shí)間就執(zhí)行。此時(shí)如果主線程里面如果還有其他任務(wù)那么就不會(huì)去執(zhí)行它雇逞,如果發(fā)生這種情況就會(huì)阻塞頁面渲染荤懂,因?yàn)殇秩臼且鹊侥_本任務(wù)執(zhí)行完成之后才會(huì)進(jìn)行。而requestAnimateFrame
是發(fā)生在渲染的時(shí)候塘砸,它能明確的知道動(dòng)畫什么時(shí)候開始节仿,什么時(shí)候執(zhí)行。
為什么會(huì)是將近3倍谣蠢?
剛開始理解這一塊的時(shí)候一直有疑問粟耻,為什么之前setTimeout
延遲參數(shù)為0的時(shí)候只快3倍查近。因?yàn)榘凑彰棵?0次刷新的頻率均攤下來的話眉踱,每次也應(yīng)該要多執(zhí)行1000 / 60大概16.7次才對(duì)。后來查閱一些資料才知道:setTimeout
的延遲參數(shù)有一個(gè)默認(rèn)的最小值4.7霜威。當(dāng)不傳延遲參數(shù)谈喳,或者小于4.7的時(shí)候,setTimeout就是使用默認(rèn)的4.7戈泼。所以真正多渲染的次數(shù)應(yīng)該是16.7 / 4.7婿禽,將近3~4倍赏僧。