事件循環(Event Loop)promise、setTimeout、async的先后執行順序


javaScript的特點就是單線程,在這個線程中擁有唯一的一個事件循環。

  • JS分為同步任務和異步任務,同步任務都在主線程上執行。前一個任務執行完畢之后,執行后一個任務,形成一個執行棧
  • 主線程之外,事件觸發線程管理着一個任務隊列,異步任務會被主線程掛起,不會進入主線程,而是進入任務隊列。只要異步任務有了運行結果,就會在隊列任務中放置一個事件;
  • 一旦執行棧中所有的同步任務執行完畢后,系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

為什么JS是單線程的??

JS的主要用途就是與用戶交互,操作DOM,假如JS同時有兩個線程,一個線程中在某個DOM節點上添加內容,另一個線程需要執行刪除該節點操作,就會產生沖突。

事件循環機制告訴我們JavaScript的執行順序。

單線程意味着所有任務都需要排隊,前一任務結束,才會執行后一個任務,如果前一個任務耗時很長,后一個任務就不得不一直等着。

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)

 

先按順序執行同步代碼  從‘script start‘開始,

執行到setTimeout函數時,將其回調函數加入隊列(此隊列與promise隊列不是同一個隊列,執行的優先級低於promise。

然后調用async1()方法,await async2();//執行這一句后,輸出async2后,await會讓出當前線程,將后面的代碼加到任務隊列中,然后繼續執行test()函數后面的同步代碼

繼續執行創建promise對象里面的代碼屬於同步代碼,promise的異步性體現在then與catch處,所以promise1被輸出,然后將then函數的代碼加入隊列,繼續執行同步代碼,輸出script end。至此同步代碼執行完畢。

開始從隊列中調取任務執行,由於剛剛提到過,setTimeout的任務隊列優先級低於promise隊列,所以首先執行promise隊列的第一個任務,因為在async函數中有await表達式,會使async函數暫停執行,等待表達式中的 Promise 解析完成后繼續執行 async 函數並返回解決結果。

所以先執行then方法的部分,輸出promise2,然后執行async1中await后面的代碼,輸出async1 end。。最后promise隊列中任務執行完畢,再執行setTimeout的任務隊列,輸出settimeout。

setTimeout(fn,0)的含義是指某個任務在主線程最早可得的空閑時間執行。它在“任務隊列”的尾部添加一個事件,因此要等到同步任務和“任務隊列”現有的時間處理完才會得到執行。

 

按照事件循環機制分析以上代碼運行流程:

  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`,取出直接輸出即可,至此整個流程結束。

 

 

 


免責聲明!

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



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