事件迴圈 EventLoop(Promise,setTimeOut,async/await執行順序)

BIGXue發表於2021-02-16

什麼是事件迴圈?想要了解什麼是事件迴圈就要從js的工作原理開始說起:

JS主要的特點就是單執行緒,所謂單執行緒就是程式中只有一個執行緒在執行。

為什麼JS是單執行緒的而不是多執行緒的呢?

JS的主要用途就是與使用者互動,操作DOM,假設JS同時有兩個執行緒,一個執行緒中在某個DOM節點上新增或者修改內容,而另一個執行緒在這個DOM節點上執行刪除該節點操作,這樣就會產生衝突。

單執行緒就意味著所有任務都需要排隊,前一任務結束,才會執行後一個任務,當是如果當遇到前一個任務耗時很長的情況,後一個任務就不得不一直等著。因此,就有了同步任務、非同步任務。

同步任務和非同步任務在js中是如何執行的呢?

js的程式碼執行會形成一個主執行緒和一個任務佇列。主執行緒會自上而下依次執行我們的js程式碼,形成一個執行棧。

同步任務就會被放到這個主執行緒中依次執行。而非同步任務被放入到任務佇列中執行,執行完就會在任務佇列中打一個標記,形成一個對應的事件。當主執行緒中的任務全部執行完畢,js會去提取並執行任務佇列中的事件。這個過程是迴圈進行的,這就是EventLoop。

JS引擎執行非同步程式碼不用等待,是因為有事件佇列和事件迴圈。

事件迴圈是指主執行緒重複從事件佇列中取訊息、執行的過程。指整個執行流程。

事件佇列是一個儲存著待執行任務的序列,其中的任務嚴格按照時間先後順序執行,排在隊頭的任務會率先執行,而排在隊尾的任務會最後執行。(即先進先出)

事件佇列:

  • 一個執行緒中,事件迴圈是唯一的,但是任務佇列可以有多個;
  • 任務佇列又分macro-task(巨集任務)和micro-task(微任務);
  • macro-task包括:script(整體程式碼)、setTimeout、setInterval、setImmediate、I/O、UI rendering;
  • micro-task包括:process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等稱為任務源,而進入任務佇列的是他們制定的具體執行任務;來自不同任務源的任務會進入到不同的任務佇列,其中setTimeout與setInterval是同源的。
 

事件迴圈執行機制

(1)執行一個巨集任務(棧中沒有就從事件佇列中獲取)

(2)執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中;

(3)巨集任務執行完畢後,立即執行當前微任務佇列的所有微任務;

(4)當前微任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染;

(5)渲染完畢後,JS執行緒繼續接管,開始下一個巨集任務。

 

 

事例:

                 async function async1() {           
                     console.log("async1 start");  //(2)        
                     await  async2();            
                     console.log("async1 end");   //(6)    
                 }        
                 async  function async2() {          
                     console.log( 'async2');   //(3)     
                 }       
                 console.log("script start");  //(1)      
                 setTimeout(function () {            
                     console.log("settimeout");  //(8)      
                 },0);        
                 async1();        
                 new Promise(function (resolve) {           
                     console.log("promise1");   //(4)         
                     resolve();        
                 }).then(function () {            
                     console.log("promise2");    //(7)    
                 });        
                 console.log('script end');//(5)

 

按照事件迴圈機制分析以上程式碼執行流程:

  1. 首先,事件迴圈從巨集任務(macrotask)佇列開始,首先讀取script(整體程式碼)任務;當遇到任務源(task source)時,則會先分發任務到對應的任務佇列中去。

  2. 然後我們看到首先定義了兩個async函式,此時沒有呼叫,接著往下看,然後遇到了 `console` 語句,直接輸出 `script start`。輸出之後,script 任務繼續往下執行,遇到 `setTimeout`,其作為一個巨集任務源,則會先將其任務分發到對應的任務佇列中。

  3. script 任務繼續往下執行,執行了async1()函式,async函式中在await之前的程式碼是立即執行的,所以會立即輸出`async1 start`。
遇到了await時,會將await後面的表示式執行一遍,所以就緊接著輸出`async2`,然後將await後面的程式碼也就是`console.log('async1 end')`加入到microtask中的Promise佇列中,接著跳出async1函式來執行後面的程式碼。

  4. script任務繼續往下執行,遇到Promise例項。由於Promise中的函式是立即執行的,而後續的 `.then` 則會被分發到 microtask 的 `Promise` 佇列中去。所以會先輸出 `promise1`,然後執行 `resolve`,將 `promise2` 分配到對應佇列。

  5. script任務繼續往下執行,輸出了 `script end`,至此,全域性任務就執行完畢了。
根據上述,每次執行完一個巨集任務之後,會去檢查是否存在 Microtasks;如果有,則執行 Microtasks 直至清空 Microtask Queue。
因而在script任務執行完畢之後,開始查詢清空微任務佇列。此時,微任務中, `Promise` 佇列有的兩個任務`async1 end`和`promise2`,因此按事件佇列先進先出的原則,先後順序輸出 `async1 end,promise2`。當所有的 Microtasks 執行完畢之後,表示第一輪的迴圈就結束了。

  6. 第二輪迴圈依舊從巨集任務佇列開始。此時巨集任務中只有一個 `setTimeout`,取出直接輸出即可,至此整個流程結束。

相關文章