2021-11-12 requestAnimationFrame 執(zhí)行機制探索

requestAnimationFrame 執(zhí)行機制探索

[TOC]

什么是 requestAnimationFrame

window.requestAnimationFrame() 告訴瀏覽器——你希望執(zhí)行一個動畫谍肤,并且要求瀏覽器在下次重繪之前調用指定的回調函數(shù)更新動畫。

該方法需要傳入一個回調函數(shù)作為參數(shù)鸡典,該回調函數(shù)會在瀏覽器下一次重繪之前執(zhí)行溶握。根據(jù)以上 MDN 的定義挠将,requestAnimationFrame 是瀏覽器提供的一個按幀對網(wǎng)頁進行重繪的 API 颠蕴。

先看下面這個例子迫皱,了解一下它是如何使用并運行的

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
function animation() {
  if (i > 200) return;
  test.style.marginLeft = `${i}px`;
  window.requestAnimationFrame(animation);
  i++;
}
window.requestAnimationFrame(animation);

上面的代碼 1s 大約執(zhí)行 60 次摊腋,因為一般的屏幕硬件設備的刷新頻率都是 60Hz妄痪,然后每執(zhí)行一次大約是 16.6ms哈雏。使用 requestAnimationFrame 的時候,只需要反復調用它就可以實現(xiàn)動畫效果衫生。

同時 requestAnimationFrame 會返回一個請求 ID裳瘪,是回調函數(shù)列表中的一個唯一值,可以使用 cancelAnimationFrame 通過傳入該請求 ID 取消回調函數(shù)罪针。

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let requestId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  requestId = requestAnimationFrame(animation);
  i++;
  if (i > 200) {
    cancelAnimationFrame(requestId);
  }
}
animation();

下圖 1 是上面例子的執(zhí)行結果:

640.gif

requestAnimationFrame 執(zhí)行困惑

使用 JavaScript 實現(xiàn)動畫的方式還可以使用 setTimeout 彭羹,下面是實現(xiàn)的代碼

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let timerId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  // 執(zhí)行間隔設置為 0,來模仿 requestAnimationFrame
  timerId = setTimeout(animation, 0);
  i++;
  if (i > 200) {
    clearTimeout(timerId);
  }
}
animation();

在這里將 setTimeout 的執(zhí)行間隔設置為 0泪酱,來模仿 requestAnimationFrame派殷。

單單從代碼上實現(xiàn)的方式,看不出有什么區(qū)別墓阀,但是從下面具體的實現(xiàn)結果就可以看出很明顯的差距了毡惜。

下圖 2 是 setTimeout 執(zhí)行結果

640 (1).gif

很明顯能看出,setTimeoutrequestAnimationFrame 實現(xiàn)的動畫“快”了很多斯撮。這是什么原因呢经伙?

可能你也猜到了,Event LooprequestAnimationFrame 在執(zhí)行的時候有些特殊的機制吮成,下面就來探究一下 Event LooprequestAnimationFrame 的關系橱乱。

Event Loop 與 requestAnimationFrame

Event Loop(事件循環(huán))是用來協(xié)調事件、用戶交互粱甫、腳本泳叠、渲染、網(wǎng)絡的一種瀏覽器內部機制茶宵。

Event Loop 在瀏覽器內也分幾種:

  • window event loop
  • worker event loop
  • worklet event loop

我們這里主要討論的是 window event loop危纫。也就是瀏覽器一個渲染進程內主線程所控制的 Event Loop

task queue

一個 Event Loop 有一個或多個 task queues。一個 task queue 是一系列 tasks 的集合种蝶。

注:一個 task queue 在數(shù)據(jù)結構上是一個集合契耿,而不是隊列,因為事件循環(huán)處理模型會從選定的 task queue 中獲取第一個可運行任務(runnable task)螃征,而不是使第一個 task 出隊搪桂。上述內容來自 HTML 規(guī)范。這里讓人迷惑的是盯滚,明明是集合踢械,為啥還叫“queue”啊 T.T

task

一個 task 可以有多種 task sources (任務源),有哪些任務源呢魄藕?來看下規(guī)范里的 Gerneric task sources

  • DOM 操作任務源内列,比如一個元素以非阻塞的方式插入文檔
  • 用戶交互任務源,用戶操作(比如 click)事件
  • 網(wǎng)絡任務源背率,網(wǎng)絡 I/O 響應回調
  • history traversal 任務源话瞧,比如 history.back()

除此之外還有像 Timers (setTimeoutsetInterval 等)寝姿、IndexDB 操作也是 task source交排。

microtask

一個 event loop 有一個 microtask queue,不過這個 “queue” 它確實就是那個 FIFO 的隊列会油。

規(guī)范里沒有指明哪些是 microtask 的任務源个粱,通常認為以下幾個是 microtask

  • promises
  • MutationObserver
  • Object.observe
  • process.nextTick (這個東西是 Node.js 的 API,暫且不討論)

Event Loop 處理過程

  1. 在所選 task queue (taskQueue)中約定必須包含一個可運行任務翻翩。如果沒有此類 task queue都许,則跳轉至下面 microtasks 步驟。
  2. taskQueue中最老的 task (oldestTask) 變成第一個可執(zhí)行任務嫂冻,然后從 taskQueue 中刪掉它胶征。
  3. 將上面 oldestTask 設置為 event loop 中正在運行的 task。
  4. 執(zhí)行 oldestTask桨仿。
  5. event loop 中正在運行的 task 設置為 null睛低。
  6. 執(zhí)行 microtasks 檢查點(也就是執(zhí)行 microtasks 隊列中的任務)。
  7. 設置 hasARenderingOpportunity 為 false服傍。
  8. 更新渲染钱雷。
  9. 如果當前是 window event looptask queues 里沒有 task 且 microtask queue 是空的,同時渲染時機變量 hasARenderingOpportunity 為 false 吹零,去執(zhí)行 idle period(requestIdleCallback)罩抗。
  10. 返回到第一步。

大體上來說灿椅,event loop 就是不停地找 task queues 里是否有可執(zhí)行的 task 套蒂,如果存在即將其推入到 call stack (執(zhí)行棧)里執(zhí)行钞支,并且在合適的時機更新渲染。

下圖 3 是 event loop 在瀏覽器主線程上運行的一個清晰的流程

image.png

在上面規(guī)范的說明中操刀,渲染的流程是在執(zhí)行 microtasks 隊列之后烁挟,更進一步,再來看看渲染的處理過程骨坑。

更新渲染

  1. 遍歷當前瀏覽上下文中所有的 document 撼嗓,必須按在列表中找到的順序處理每個 document
  2. 渲染時機(Rendering opportunities):如果當前瀏覽上下文中沒有到渲染時機則將所有 docs 刪除卡啰,取消渲染(此處是否存在渲染時機由瀏覽器自行判斷静稻,根據(jù)硬件刷新率限制、頁面性能或頁面是否在后臺等因素)匈辱。
  3. 如果當前文檔不為空,設置 hasARenderingOpportunity 為 true 杀迹。
  4. 不必要的渲染(Unnecessary rendering):如果瀏覽器認為更新文檔的瀏覽上下文的呈現(xiàn)不會產生可見效果且文檔的 animation frame callbacks 是空的亡脸,則取消渲染。(終于看見 requestAnimationFrame 的身影了
  5. 從 docs 中刪除瀏覽器認為出于其他原因最好跳過更新渲染的文檔树酪。
  6. 如果文檔的瀏覽上下文是頂級瀏覽上下文浅碾,則刷新該文檔的自動對焦候選對象。
  7. 處理 resize 事件续语,傳入一個 performance.now() 時間戳垂谢。
  8. 處理 scroll 事件,傳入一個 performance.now() 時間戳疮茄。
  9. 處理媒體查詢滥朱,傳入一個 performance.now() 時間戳。
  10. 運行 CSS 動畫力试,傳入一個 performance.now() 時間戳徙邻。
  11. 處理全屏事件,傳入一個 performance.now() 時間戳畸裳。
  12. 執(zhí)行 requestAnimationFrame 回調缰犁,傳入一個 performance.now() 時間戳。
  13. 執(zhí)行 intersectionObserver 回調怖糊,傳入一個 performance.now() 時間戳帅容。
  14. 對每個 document 進行繪制。
  15. 更新 ui 并呈現(xiàn)伍伤。

至此并徘,requestAnimationFrame 的回調時機就清楚了,它會在 style/layout/paint 之前調用嚷缭。

再回到文章開始提到的 setTimeout 動畫比 requestAnimationFrame 動畫更快的問題饮亏,這就很好解釋了耍贾。

首先,瀏覽器渲染有個渲染時機(Rendering opportunity)的問題路幸,也就是瀏覽器會根據(jù)當前的瀏覽上下文判斷是否進行渲染荐开,它會盡量高效,只有必要的時候才進行渲染简肴,如果沒有界面的改變晃听,就不會渲染。

按照規(guī)范里說的一樣砰识,因為考慮到硬件的刷新頻率限制能扒、頁面性能以及頁面是否存在后臺等等因素,有可能執(zhí)行完 setTimeout 這個 task 之后辫狼,發(fā)現(xiàn)還沒到渲染時機初斑,所以 setTimeout 回調了幾次之后才進行渲染,此時設置的 marginLeft 和上一次渲染前 marginLeft 的差值要大于 1px 的膨处。

下圖 5 是 setTimeout 執(zhí)行情況见秤,紅色圓圈處是兩次渲染,中間四次是處理 setTimout task真椿,因為屏幕的刷新頻率是 60 Hz鹃答,所以大致在 16.6ms 之內執(zhí)行了多次 setTimeout task 之后才到了渲染時機并執(zhí)行渲染。

image.png

requestAnimationFrame 幀動畫不同之處在于突硝,每次渲染之前都會調用测摔,此時設置的 marginLeft 和上一次渲染前 marginLeft 的差值為 1px 。

下圖 6 是 requestAnimationFrame 執(zhí)行情況解恰,每次調用完都會執(zhí)行渲染:

image.png
image.png

所以看上去 setTimeout “快”了很多锋八。

其他應用

使用 setTimeout 來執(zhí)行動畫之類的視覺變化,很可能導致丟幀修噪,導致卡頓查库,所以應盡量避免使用 setTimeout 來執(zhí)行動畫,推薦使用 requestAnimationFrame 來替換它

requestAnimationFrame 除了用來實現(xiàn)動畫的效果黄琼,還可以用來實現(xiàn)對大任務的分拆執(zhí)行

執(zhí)行 JavaScript task 是在渲染之前樊销,如果在一幀之內 JavaScript 執(zhí)行時間過長就會阻塞渲染,同樣會導致丟幀脏款、卡頓

針對這種情況可以將 JavaScript task 劃分為各個小塊围苫,并使用 requestAnimationFrame() 在每個幀上運行。如下例所示

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
  var taskFinishTime;
  do {
    // 假設下一個任務被壓入 call stack
    var nextTask = taskList.pop();
    // 執(zhí)行下一個 task
    processTask(nextTask);
    // 如何時間足夠繼續(xù)執(zhí)行下一個
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
  if (taskList.length > 0) {
    requestAnimationFrame(processTaskList);
  }
}

原文鏈接:requestAnimationFrame 執(zhí)行機制探索

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末撤师,一起剝皮案震驚了整個濱河市剂府,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剃盾,老刑警劉巖腺占,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淤袜,死亡現(xiàn)場離奇詭異,居然都是意外死亡衰伯,警方通過查閱死者的電腦和手機铡羡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來意鲸,“玉大人烦周,你說我怎么就攤上這事≡豕耍” “怎么了读慎?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長槐雾。 經常有香客問我夭委,道長,這世上最難降的妖魔是什么蚜退? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任闰靴,我火速辦了婚禮,結果婚禮上钻注,老公的妹妹穿的比我還像新娘。我一直安慰自己配猫,他們只是感情好幅恋,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泵肄,像睡著了一般捆交。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上腐巢,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天品追,我揣著相機與錄音,去河邊找鬼冯丙。 笑死肉瓦,一個胖子當著我的面吹牛,可吹牛的內容都是我干的胃惜。 我是一名探鬼主播泞莉,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼船殉!你這毒婦竟也來了鲫趁?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤利虫,失蹤者是張志新(化名)和其女友劉穎挨厚,沒想到半個月后堡僻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡疫剃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年钉疫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慌申。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡陌选,死狀恐怖,靈堂內的尸體忽然破棺而出蹄溉,到底是詐尸還是另有隱情咨油,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布柒爵,位于F島的核電站役电,受9級特大地震影響,放射性物質發(fā)生泄漏棉胀。R本人自食惡果不足惜法瑟,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望唁奢。 院中可真熱鬧霎挟,春花似錦、人聲如沸麻掸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脊奋。三九已至熬北,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诚隙,已是汗流浹背讶隐。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留久又,地道東北人巫延。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像籽孙,于是被迫代替她去往敵國和親烈评。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內容