setTimeout和requestAnimationFrame

開篇一道題

setTimeout(() => {
    console.log(1);
}, 0)
console.log(2);

答案:輸出 2 , 1。javascript

目錄

  • 單線程模型
  • 任務隊列
  • setTimeout
  • setTimeoutsetInterval
  • requestAnimationFrame
  • requestidlecallback

單線程模型

JavaScript語言的一大特色就是單線程,也就是說,同一時間只能作一件事,前面的任務沒作完,後面的任務只能等着。html

爲何JavaScript是單線程的呢?

這主要與JavaScript用途有關。它的主要用途是與用戶互動,以及操做DOM。若是JavaScript是多線程的,會帶來不少複雜的問題,假如 JavaScript有A和B兩個線程,A線程在DOM節點上添加了內容,B線程刪除了這個節點,應該是哪一個爲準呢? 因此,爲了不復雜性,因此設計成了單線程。前端

雖然 HTML5 提出了Web Worker標準。Web Worker 的做用,就是爲 JavaScript 創造多線程環境,容許主線程建立 Worker 線程,將一些任務分配給後者運行。可是子線程徹底不受主線程控制,且不得操做DOM。因此這個並無改變JavaScript單線程的本質。通常使用 Web Worker 的場景是代碼中有不少計算密集型或高延遲的任務,能夠考慮分配給 Worker 線程。
可是使用的時候必定要注意,worker 線程是爲了讓你的程序跑的更快,可是若是 worker 線程和主線程之間通訊的時間大於了你不使用worker線程的時間,結果就得不償失了。java

瀏覽器下的JavaScript

瀏覽器的內核是多進程的

  • brower進程(主進程)react

    • 負責瀏覽器的頁面展現,與用戶交互。如前進,後退
    • 頁面的前進,後退
    • 負責頁面的管理,建立和銷燬其餘進程
  • GPU進程web

    • 3D渲染
  • 插件進程瀏覽器

    • 每種類型的插件對應一個進程,僅當使用該插件時才能建立
  • 瀏覽器渲染進程(瀏覽器內核)微信

    • GUI渲染進程多線程

      • DOM解析, CSS解析,生成渲染樹
    • js引擎線程架構

      • 執行Js代碼
    • 事件觸發

      • 管理着一個任務隊列
    • 異步HTTP請求線程
    • 定時觸發器線程

能夠看到 js引擎是瀏覽器渲染進程的一個線程。

瀏覽器內核中線程之間的關係

  • GUI渲染線程和JS引擎線程互斥

    • js是能夠操做DOM的,若是在修改這些元素的同時渲染頁面(js線程和ui線程同時運行),那麼渲染線程先後得到的元素數據可能就不一致了。
  • JS阻塞頁面加載

    • js若是執行時間過長就會阻塞頁面

瀏覽器是多進程的優勢

  • 默認新開 一個 tab 頁面 新建 一個進程,因此單個 tab 頁面崩潰不會影響到整個瀏覽器。
  • 第三方插件崩潰也不會影響到整個瀏覽器。
  • 多進程能夠充分利用現代 CPU 多核的優點。
  • 方便使用沙盒模型隔離插件等進程,提升瀏覽器的穩定性。

進程和線程又是什麼呢

進程(process)和線程(thread)是操做系統的基本概念。

  • 進程是 CPU 資源分配的最小單位(是能擁有資源和獨立運行的最小單位)。
  • 線程是 CPU 調度的最小單位(是創建在進程基礎上的一次程序運行單位)。

因爲每一個進程至少要作一件事,因此一個進程至少有一個線程。系統會給每一個進程分配獨立的內存,所以進程有它獨立的資源。同一進程內的各個線程之間共享該進程的內存空間(包括代碼段,數據集,堆等)。
進程能夠理解爲一個工廠不不一樣車間,相互獨立。線程是車間裏的工人,能夠本身作本身的事情,也能夠相互配合作同一件事情。

若是你想知道更多,推薦看 《WebKit技術內幕》這本書。

任務隊列

單線程就意味着,全部任務都要排隊執行,前一個任務結束,纔會執行後一個任務。若是一個任務須要執行,但此時JavaScript引擎正在執行其餘任務,那麼這個任務就須要放到一個隊列中進行等待。等到線程空閒時,就能夠從這個隊列中取出最先加入的任務進行執行(相似於咱們去銀行排隊辦理業務,單線程至關於說這家銀行只有一個服務窗口,一次只能爲一我的服務,後面到的就須要排隊,而任務隊列就是排隊區,先到的就優先服務)
注意:若是當前線程空閒,而且隊列爲空,那每次加入隊列的函數將當即執行。

setTimeout

setTimeout的運行機制:執行該語句時,設置一個定時器,定時時間置爲多設置的延時,當計數結束後,將傳入的函數加入任務隊列,以後的執行就交給任務隊列負責。

如今咱們回到最開始的一個例子

setTimeout(() => {
    console.log(1);
}, 0)
console.log(2);

輸出 2, 1;

setTimeout的第二個參數表示在執行代碼前等待的毫秒數。上面代碼中,設置爲0,表面意思爲 執行代碼前等待的毫秒數爲0,即當即執行。但實際上的運行結果咱們也看到了,並非表面上看起來的樣子,千萬不要被欺騙了。

實際上,上面的代碼並非當即執行的,這是由於setTimeout有一個最小執行時間,HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔)不得低於4毫秒。 當指定的時間低於該時間時,瀏覽器會用最小容許的時間做爲setTimeout的時間間隔,也就是說即便咱們把setTimeout的延遲時間設置爲0,實際上可能爲 4毫秒後才事件推入任務隊列

setTimeout(() => {
    console.log(111);
}, 100);

上面代碼表示100ms後執行console.log(111),但實際上實行的時間確定是大於100ms後的, 100ms 只是表示 100ms 後將任務加入到"任務隊列"中,必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證,回調函數必定會在setTimeout()指定的時間執行。

setTimeout 和 setInterval區別

  • setTimeout: 指定延期後調用函數,每次setTimeout計時到後就會去執行,而後執行一段時間後才繼續setTimeout,中間就多了偏差,(偏差多少與代碼的執行時間有關)。
  • setInterval:以指定週期調用函數,而setInterval則是每次都精確的隔一段時間推入一個事件(可是,事件的執行時間不必定就不許確,還有多是這個事件還沒執行完畢,下一個事件就來了).

下面的例子引用 深刻理解定時器系列第一篇——理解setTimeout和setInterval 這篇文章的例子

btn.onclick = function(){
    setTimeout(function(){
        console.log(1);
    },250);
}

點擊該按鈕後,首先將onclick事件處理程序加入隊列。該程序執行後才設置定時器,再有250ms後,指定的代碼才被添加到隊列中等待執行。
若是上面代碼中的onclick事件處理程序執行了300ms,那麼定時器的代碼至少要在定時器設置以後的300ms後纔會被執行。隊列中全部的代碼都要等到javascript進程空閒以後才能執行,而無論它們是如何添加到隊列中的。

如圖所示,儘管在255ms處添加了定時器代碼,但這時候還不能執行,由於onclick事件處理程序仍在運行。定時器代碼最先能執行的時機是在300ms處,即onclick事件處理程序結束以後。

setInterval存在的一些問題:

JavaScript中使用 setInterval 開啓輪詢。定時器代碼可能在代碼再次被添加到隊列以前尚未完成執行,結果致使定時器代碼連續運行好幾回,而之間沒有任何停頓。而javascript引擎對這個問題的解決是:當使用setInterval()時,僅當沒有該定時器的任何其餘代碼實例時,纔將定時器代碼添加到隊列中。這確保了定時器代碼加入到隊列中的最小時間間隔爲指定間隔。

可是,這樣會致使兩個問題:

  • 一、某些間隔被跳過;
  • 二、多個定時器的代碼執行之間的間隔可能比預期的小

假設,某個onclick事件處理程序使用setInterval()設置了200ms間隔的定時器。若是事件處理程序花了300ms多一點時間完成,同時定時器代碼也花了差很少的時間,就會同時出現跳過某間隔的狀況

例子中的第一個定時器是在205ms處添加到隊列中的,可是直到過了300ms處才能執行。當執行這個定時器代碼時,在405ms處又給隊列添加了另外一個副本。在下一個間隔,即605ms處,第一個定時器代碼仍在運行,同時在隊列中已經有了一個定時器代碼的實例。結果是,在這個時間點上的定時器代碼不會被添加到隊列中

使用setTimeout構造輪詢能保證每次輪詢的間隔。

setTimeout(function () {
 console.log('我被調用了');
 setTimeout(arguments.callee, 100);
}, 100);
callee 是 arguments 對象的一個屬性。它能夠用於引用該函數的函數體內當前正在執行的函數。在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當一個函數必須調用自身的時候, 避免使用 arguments.callee(), 經過要麼給函數表達式一個名字,要麼使用一個函數聲明.
setTimeout(function fn(){
    console.log('我被調用了');
    setTimeout(fn, 100);
},100);

這個模式鏈式調用了setTimeout(),每次函數執行的時候都會建立一個新的定時器。第二個setTimeout()調用當前執行的函數,併爲其設置另一個定時器。這樣作的好處是,在前一個定時器代碼執行完以前,不會向隊列插入新的定時器代碼,確保不會有任何缺失的間隔。並且,它能夠保證在下一次定時器代碼執行以前,至少要等待指定的間隔,避免了連續的運行。

requestAnimationFrame

60fps與設備刷新率

目前大多數設備的屏幕刷新率爲60次/秒,若是在頁面中有一個動畫或者漸變效果,或者用戶正在滾動頁面,那麼瀏覽器渲染動畫或頁面的每一幀的速率也須要跟設備屏幕的刷新率保持一致。

卡頓:其中每一個幀的預算時間僅比16毫秒多一點(1秒/ 60 = 16.6毫秒)。但實際上,瀏覽器有整理工做要作,所以您的全部工做是須要在10毫秒內完成。若是沒法符合此預算,幀率將降低,而且內容會在屏幕上抖動。此現象一般稱爲卡頓,會對用戶體驗產生負面影響。

跳幀: 假如動畫切換在 16ms, 32ms, 48ms時分別切換,跳幀就是假如到了32ms,其餘任務還未執行完成,沒有去執行動畫切幀,等到開始進行動畫的切幀,已經到了該執行48ms的切幀。就比如你玩遊戲的時候卡了,過了一會,你再看畫面,它不會停留你卡的地方,或者這時你的角色已經掛掉了。必須在下一幀開始以前就已經繪製完畢;

Chrome devtool 查看實時 FPS, 打開 More tools => Rendering, 勾選 FPS meter

requestAnimationFrame實現動畫

requestAnimationFrame是瀏覽器用於定時循環操做的一個接口,相似於setTimeout,主要用途是按幀對網頁進行重繪。

requestAnimationFrame 以前,主要藉助 setTimeout/ setInterval 來編寫 JS 動畫,而動畫的關鍵在於動畫幀之間的時間間隔設置,這個時間間隔的設置有講究,一方面要足夠小,這樣動畫幀之間纔有連貫性,動畫效果才顯得平滑流暢;另外一方面要足夠大,確保瀏覽器有足夠的時間及時完成渲染。

顯示器有固定的刷新頻率(60Hz或75Hz),也就是說,每秒最多隻能重繪60次或75次,requestAnimationFrame的基本思想就是與這個刷新頻率保持同步,利用這個刷新頻率進行頁面重繪。此外,使用這個API,一旦頁面不處於瀏覽器的當前標籤,就會自動中止刷新。這就節省了CPU、GPU和電力。

requestAnimationFrame是在主線程上完成。這意味着,若是主線程很是繁忙,requestAnimationFrame的動畫效果會大打折扣。

requestAnimationFrame使用一個回調函數做爲參數。這個回調函數會在瀏覽器重繪以前調用。

requestID = window.requestAnimationFrame(callback);
window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
            window.setTimeout(callback, 1000 / 60);
        };
})();

上面的代碼按照1秒鐘60次(大約每16.7毫秒一次),來模擬requestAnimationFrame

requestIdleCallback()

MDN上的解釋: requestIdleCallback()方法將在瀏覽器的空閒時段內調用的函數排隊。這使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。函數通常會按先進先調用的順序執行,然而,若是回調函數指定了執行超時時間timeout,則有可能爲了在超時前執行函數而打亂執行順序。

requestAnimationFrame會在每次屏幕刷新的時候被調用,而requestIdleCallback則會在每次屏幕刷新時,判斷當前幀是否還有多餘的時間,若是有,則會調用requestAnimationFrame的回調函數,

圖片中是兩個連續的執行幀,大體能夠理解爲兩個幀的持續時間大概爲16.67,圖中黃色部分就是空閒時間。因此,requestIdleCallback中的回調函數僅會在每次屏幕刷新而且有空閒時間時纔會被調用.

利用這個特性,咱們能夠在動畫執行的期間,利用每幀的空閒時間來進行數據發送的操做,或者一些優先級比較低的操做,此時不會使影響到動畫的性能,或者和requestAnimationFrame搭配,能夠實現一些頁面性能方面的的優化,

react 的 fiber 架構也是基於 requestIdleCallback 實現的, 而且在不支持的瀏覽器中提供了 polyfill

總結

  • 從單線程模型和任務隊列出發理解 setTimeout(fn, 0),並非當即執行。
  • JS 動畫, 用requestAnimationFrame 會比 setInterval 效果更好
  • requestIdleCallback()經常使用來切割長任務,利用空閒時間執行,避免主線程長時間阻塞。

參考

其餘

最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。
牧碼的星星

相關文章
相關標籤/搜索