JavaScript定時器與執行機制


  JavaScript動畫中是必須使用到定時器的,這里做一個總結。 

var label = 'someLable';

console.time(label);

console.timeEnd(label);

    通過上面的代碼,我們可以進行時間統計。

  

從JS執行機制說起(任務隊列)

  首先,需要知道的是無論是否提到了異步,JavaScript都是單線程的(注意:這里的單線程並不是真正意義上的單線程,我們所說js單線程,是說js的代碼執行只有一個線程,但是比如js使用過程中,會用到一些I/O操作,這些還是需要I/O線程的。所以,這里的單線程是執行代碼始終只有一個線程。),作為腳本語言,也不允許其出現多線程,否則,對於DOM的操作就會引起混亂,當然,現在出現的web worker確實提供了JavaScript創建多個線程的能力,但是子線程是完全收到主線程控制的,並且不得操作DOM。 所以,這個新標准沒有改變JavaScript單線程的本質。

  既然是單線程那么同一時間內只能執行一個任務,其他任務就得排隊,后續任務必須等到前一個任務執行完才能開始執行。

為了避免因為某些長時間任務造成的無意義的等待,js引入了異步的概念

OK! 那么究竟什么是無意義的等待呢? 我們應該怎么理解呢?

  • 比如讀取文件的時候,這是cpu幾乎是沒有占用的,但是讀取文件在耗費大量的時間,如果不采取異步的方式,那么cpu的利用率就會很低。 即異步操作的大部分時間cpu是幫不上忙的。 
  • 比如發送ajax請求的時候,如果采用同步,那么cpu就需要一直等着他,如果網絡不好,可能就要等很久,並且cpu是幫不了忙的,因此采用異步,等到他做完了,通知cpu就行了啊,或者cpu主動去找也行啊。
  • 就像之前說過了同步、異步單線程、異步多線程的區別是一樣的。 你(這里的你就是cpu)是一個廚師,分給你煮雞蛋和做烤面包的任務。如果同步,就是先煮雞蛋,然后在鍋旁邊等着它煮熟,你什么都不做,但是這是cpu也沒有用上啊。 那么異步單線程呢? 這時,你(cpu)可以先煮雞蛋,然后設置一個定時器(比如煮水好了,就會發出尖叫聲),然后去烤面包,也設置一個定時器(方式不重要,反正一定是會提醒的)。 這時候你就可以去做其他事情了(cpu在主線程上執行后續任務),而不用等着那些你幫不上忙的事情。等到提醒你(cpu)都煮熟的時候,(cpu知道了就會將之加入主線程處理),你再拿着這些東西送給客人。   而異步多線程呢?這個也是很好理解的,就是你(cpu)可以多雇佣兩個人(兩個線程),一個人負責煮雞蛋(只看着煮雞蛋,別的什么都不做),另外一個人負責烤面包(只烤面包,其他的什么都不做),你負責協調。 嗯。 
  • 同步任務會直接在主線程中順序執行。 
  • 異步任務需要先排到一個任務隊列中去,不會阻塞主線程。等到主線程執行完了之后,就會去查詢在異步的任務隊列中是否有可以執行的任務(異步可能還需要等待一些ajax請求,文件讀寫),一旦這些異步任務可以執行了,就會將它添加到主線程隊列中,以此循環。 

 注意: 這里所說的任務隊列一定是和異步任務有關的,而同步任務只會在主線程中排隊執行,不會涉及到任務隊列的概念

  


 

 

事件和回調函數

  “任務隊列”是一個事件的隊列(也可以理解為消息的隊列)IO設備完成了一項任務,就在“任務隊列”中添加一個事件,表示相關的異步任務可以進入主線程(執行棧)了,主線程讀取任務隊列,就是讀取里面有哪些事件。  

  “任務隊列”中的事件,除了IO設備的事件之外,還包括了一些用戶產生的事件(比如鼠標點擊、頁面滾動等),這些事件都會添加到任務隊列中去。注意:前提條件是這些事件都有相應的回調函數,其他的異步也是如此,有相應的回調函數,然后有事件才能被添加到任務隊列中區)。   

  所謂回調函數, 就是那些被主線程掛起的代碼(不會被理解執行),異步任務必須制定回調函數,當主線程開始執行異步任務時,就是執行對應的回調函數。 

  “任務隊列”是一個先進先出的數據結構,排在前面的事件,就優先被主線程讀取。 主線程的讀取過程基本上是自動的,只要執行棧一清空,“任務隊列”上第一位的事件就會自動進入主進程。 但是,由於存在后文提到的“定時器”功能,主線程要先檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程(這些時間就是我們所說的定時器)。

  

  


 

Event Loop

   由於主線程從“任務隊列”中讀取事件的這個過程是不斷循環的,所以整個的這種運行機制又稱為 Event Loop(事件循環)。 

   主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done), 顯然,任務隊列中有各種事件,也都是主線程在調用api的時候放進去的。只要棧中的代碼執行完畢(關鍵,一定要是等到棧中的代碼執行完畢),主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。

 

 


 

定時器

  JS的定時器目前有三個:setTimeout、setInterval和setImmediate。

  定時器也是一種異步任務,通常瀏覽器都有一個獨立的定時器模塊,定時器的延遲時間由定時器模塊來管理, 當某個定時器到了可執行狀態,就會被加入到主線程之中。

  

 

setTimeout

  setTimeout(fn, x)表示延遲x毫秒之后執行fn,但是使用的時候,千萬不要太相信預期,延遲的時間嚴格來說總是大於x毫秒的,至於大多少,就要看當時JavaScript的執行情況了。

  另外,如果多個定時器如果沒有及時清除,就會存在干擾,總之,及時清除已經不需要的定時器是一個好的習慣。

HTML5規范規定了最小延遲時間不得小於4ms,即如果x小於4,會被當做4來處理。

   setTimeout注冊的函數fn會交給瀏覽器的定時模塊來處理,延遲時間到了就會添加fn這個回調函數到主進程隊列當中,但是如果主進程隊列前面剛好還有正在執行的沒有執行完的代碼,則又要花費一定的時間去等待主進程,然后再執行fn,所以實際的時間往往是更長的。

   如果fn之前在主進程中剛好有一個超級大的循環,那么延遲就不是一點了: 

(function testSetTimeout() {
    const label = 'setTimeout';
    console.time(label);
    setTimeout(() => {
        console.timeEnd(label);
    }, 10);
    for(let i = 0; i < 10000000000; i++) {}
})();

  最后執行結果如下:

setTimeout: 12131.85986328125ms

  即在函數中雖然我們設置了在10ms之后執行函數,但是實際上卻是 12131ms,近千倍的差距就是因為主進程上還有沒有執行完的任務。下面我們進行相應的解析:

  • 首先在主進程上同步執行 第一句、第二句代碼
  •  接着,遇到了setTimeout函數,異步執行。即首先將異步執行的回調函數和這個事件放在任務隊列里,然后將時間告訴給瀏覽器的定時模塊。 
  • 然后迅速開始執行下面的for循環,由於這個循環很大,所以執行很久。在這個過程中, 瀏覽器的定時模塊在10ms之后就開始把這個回調函數放在了主線程的執行隊列中。
  • 但是主線程上的for還沒有執行完畢,所以,回調函數就需要一直等着,知道for執行完畢,主線程隊列上的回調函數開始執行。 所以時間有了一個很大的延遲。
      function fda() {
          for (var i = 0; i < 999; i++) {
              console.log(4);
              setTimeout(function () {
                console.log(5);
              }, 10);
          }
      }
      fda();

  比如上面這段代碼,就是先輸出所有的4,然后再輸出所有的5, 因為主線程的任務沒有執行完,放在任務隊列中的回調函數也就不會立即執行。

 

 

 

 

setInterval

  setInterval的實現機制跟setTimeout類似,只不過setInterval是重復執行的。

  對於setInterval(fn, 100)容易產生一個誤區: 執行完fn之后的100ms立即再次執行fn。 

  事實上,setInterval交給了瀏覽器的定時模塊,定時模塊並不會顧及fn的上一次的執行結果,而是每隔100ms就把fn放在主線程上,但是主線程上是否有任務在執行而不讓fn執行呢 ?這個瀏覽器的定時模塊就不管了,我們也無法確定。 

  

  

 

setImmediate

  這算一個比較新的定時器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他瀏覽器未測試。

  這個api的支持性並不是很好。

    從API名字來看很容易聯想到setTimeout(0),不過setImmediate應該算是setTimeout(0)的替代版。

  在IE11/Edge中,setImmediate延遲可以在1ms以內,而setTimeout有最低4ms的延遲,所以setImmediate比setTimeout(0)更早執行回調函數。不過在Nodejs中,兩者誰先執行都有可能,原因是Nodejs的事件循環和瀏覽器的略有差異。

  很明顯,setImmediate設計來是為保證讓代碼在下一次事件循環執行,以前setTimeout(0)這種不可靠的方式可以丟掉了

  總之,記住setImmediate會比setTimeout(fn, 0)更快、更及時一點就么有錯了。

  

  

requestAnimationFrame

  requestAnimationFrame並不是定時器,但和setTimeout很相似,在沒有requestAnimationFrame的瀏覽器一般都是用setTimeout模擬。

  requestAnimationFrame跟屏幕刷新同步,大多數屏幕的刷新頻率都是60Hz,對應的requestAnimationFrame大概每隔16.7ms觸發一次,如果屏幕刷新頻率更高,requestAnimationFrame也會更快觸發。基於這點,在支持requestAnimationFrame的瀏覽器還使用setTimeout做動畫顯然是不明智的。

  這一點很關鍵,requestAnimationFrame是跟着屏幕來刷新的,而不會顧及到任務隊列的事情。所以會更為及時。 

  在不支持requestAnimationFrame的瀏覽器,如果使用setTimeout/setInterval來做動畫,最佳延遲時間也是16.7ms。 如果太小,很可能連續兩次或者多次修改dom才一次屏幕刷新,這樣就會丟幀,動畫就會卡;如果太大,顯而易見也會有卡頓的感覺。

  所以,我們最好就要設置為16.7ms,如果設置的少了,還有可能出現問題,何必呢?

  

  有趣的是,第一次觸發requestAnimationFrame的時機在不同瀏覽器也存在差異,Edge中,大概16.7ms之后觸發,而Chrome則立即觸發,跟setImmediate差不多。按理說Edge的實現似乎更符合常理。

 

 

Promise

  

  Promise是很常用的一種異步模型,如果我們想讓代碼在下一個事件循環執行,可以選擇使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。

而且Promise的延遲比setImmediate更低,意味着Promise比setImmediate先執行。

function testSetImmediate() {
    const label = 'setImmediate';
    console.time(label);
 
    setImmediate(() => {
        console.timeEnd(label);
    });
}
 
function testPromise() {
    const label = 'Promise';
    console.time(label);
    new Promise((resolve, reject) => {
        resolve();
    }).then(() => {
        console.timeEnd(label);
    });
}
 
testSetImmediate();
testPromise();

 

  

  

process.nextTick

  

  process.nextTick是Nodejs的API,比Promise更早執行。

  事實上,process.nextTick是不會進入異步隊列的而是直接在主線程隊列尾強插一個任務雖然不會阻塞主線程,但是會阻塞異步任務的執行,如果有嵌套的process.nextTick,那異步任務就永遠沒機會被執行到了。

使用的時候要格外小心,除非你的代碼明確要在本次事件循環結束之前執行,否則使用setImmediate或者Promise更保險。

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM