JavaScript事件迴圈機制

有夢想的鹹魚前端發表於2021-04-26

javaScript是單執行緒的語言:

  眾所周知,javaScript是一門單執行緒語言;何為單執行緒?我的理解是:同一時間只能做同一件事;單執行緒在程式執行時,所走的程式路徑按照連續順序排下來,前面的必須處理好,後面的才會執行。

白話解釋:

  假如去某銀行辦理業務,某銀行的單次業務接待總量為100個客戶,但此時在該銀行等待辦理業務的人數為150人,所以有50人是需要等待的,等待時間是未知;銀行採取叫號方式進行排隊辦理業務,前100個人是可以完成業務辦理的,此時這100個人形成了一個排隊的佇列,在計算機中我們稱之為執行緒;只有執行緒佇列中完成了任務或者空閒,剩下的50人才能繼續辦理業務;

javaScript為什麼會是單執行緒的語言?

 在《javaScript高階程式設計》一書中有一個很好的解釋:如果JS是多執行緒語言,那麼假如當多個執行緒同時操作同一個DOM的時候,瀏覽器該如何渲染?瀏覽器該聽哪個執行緒的指令?渲染結果是否會超出預期?基於這個特性,JS必須只能是單執行緒語言;

 

程式與執行緒的區別:

  程式:是cpu資源分配的最小單位;(能擁有獨立資源和獨立執行的最小單位)

  執行緒:是cpu資源排程的最小單位;(執行緒是建立在程式基礎上一次程式最小的執行單位,一個程式中可以有多個執行緒)

  白話解釋:假設程式是一個工廠,它有它的獨立資源並且不受外部其他工廠影響,工廠之間互相是獨立存在;而執行緒是這個工廠裡面的工人,工人之間可以互相分工完成工作任務,並且工人數量可以不等,可以為1個也可以為多個,且工人之間的空間是共享的,能互相訪問到的;
 
    我們作為前端,經常打交道的瀏覽器就是一個典型的多程式的程式;瀏覽器沒開啟一個tab頁面就是一個單獨的程式,我們瀏覽器可以開啟多個tab頁面,所以就證實了瀏覽器是多程式的,每個程式之間互相獨立互不影響;其中瀏覽器中的瀏覽器渲染程式(又稱瀏覽器核心)是屬於多程式中的一種,主要負責頁面渲染、指令碼執行、事件處理等;其包含的執行緒有:GUI渲染執行緒(渲染頁面、解析HTML、CSS構成DOM樹)、JS引擎執行緒、事件觸發執行緒、定時器觸發執行緒、http請求執行緒等主要執行緒;
 
javaScript執行機制:
    上面我們說到JS引擎執行緒,是的,JS引擎執行緒是單執行緒執行的;主要職責就是負責執行js指令碼;
  js中把任務分為同步任務和非同步任務:
  同步任務:
    即在主程式立即執行且前一個任務沒有執行完成,後面的任務不能往下執行的任務;
  非同步任務:
        不進入主程式而進入任務佇列的任務,只有任務佇列通知主程式某個任務可以執行了,非同步任務才會進入主程式開始執行;
     非同步任務我們又分為巨集任務(macro-task)和微任務(micro-task):     
     巨集任務(macro-task)包括:整體程式碼script、setTimeout、setInterval
     微任務(micro-task)包括:Promise( 注意:new Promise() 是同步任務,resolve、.then、.catch等方法才是非同步任務)、process.nextTick
    tips:同一輪事件迴圈中,微任務永遠比巨集任務先執行;
  所以js的事件迴圈如下圖所示:

 

   解析圖文: 

    同步和非同步任務分別進入不同的執行"場所",同步的進入主執行緒,非同步的進入Event Table(任務佇列)並註冊函式。

    當指定的事情完成時,Event Table(任務佇列)會將這個函式移入Event Queue(事件佇列)。

    主執行緒內的任務執行完畢為空,會去Event Queue(事件佇列)讀取對應的函式,進入主執行緒執行。

    上述過程會不斷重複迴圈,也就是常說的Event Loop(事件迴圈)。

   舉個例子:

console.log('script start'); 
setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');

  輸出結果應為:

    script start
    script end
    promise1
    promise2
    setTimeout

  執行過程解析:

  首先迴圈瀏覽器會執行第一步的同步任務,當執行到setTimeout的時候發現是非同步的巨集任務然後放入到任務佇列中,接著往下執行Promise.then,發現Promise.then也是非同步任務且是微任務,繼續放入到佇列中接著往下走執行"sciprt end " ;執行完"script end "之後發現主程式的同步任務都執行完了,然後開始執行非同步任務,非同步任務任務又分為巨集任務和微任務,我們前面說過微任務會優先於巨集任務執行;所以我們繼續執行" promise1 ",然後往下走發現還有個.then的微任務沒有執行,接著往下執行" promise2 ",至此所有的微任務執行完畢我們再去執行巨集任務" setTimeout ",所以最終的執行結果應該為:script start > script end > promise1 > promise2 > setTimeout

 

  什麼?學會了?上面demo太簡單?那我們再看一下複雜一丟丟的例子來檢驗一下我們的學習成果

console.log('1');   
 setTimeout(() => {
    console.log('2')  
 }, 1000);
 new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('3');  
    }, 0);
    console.log('4');  
    resolve();
    console.log('5');  
 }).then(() => {
    console.log('6');  
 });
 console.log('7');  

  這個例子是在上面例子的基礎上加以改造進行了巢狀

     執行過程解析:
  首先瀏覽器會執行" 1 "這個同步任務,接著往下發現是非同步巨集任務,放入到事件佇列中接著往下走,Promise裡面還是有非同步巨集任務,繼續往下發現兩個同步任務,同步任務按順序執行所以先輸出" 4 "在輸出" 5 ",接著往下.then是非同步微任務,放入任務佇列中;然後發現同步任務執行" 7 ",本輪同步任務執行完畢以後會去執行非同步佇列中的微任務,所以我們接著會去執行.then方法輸出" 6 ",微任務執行完成以後瀏覽器再執行本輪迴圈中的巨集任務,所有瀏覽器再執行Promise裡面的setTimeout輸出" 3 ",最後我們這一輪事件迴圈執行完了,然後與Promise同層的" 2 "還沒有執行,還在任務佇列中,最後瀏覽器再執行且輸出" 2 ";注意,事件迴圈是分輪次的,只有本層的任務執行完以後再會往上去執行;所以我們Promise裡面的setTimeout要先於外層的setTimeout執行,這個就是因為層級的原因;所以最後的執行結果應該是:" 1 " > " 4 " > " 5 " > " 7 " > " 6 " > " 3 " > " 2 "
 
  總結:瀏覽器在執行程式碼的時候碰到同步任務會一步一步往下執行,碰到非同步任務會放入到任務佇列中等同步任務執行完以後再放入到主程式中執行;所以每一輪事件迴圈的執行順序應該都是同步優先非同步執行,非同步又分為巨集任務和微任務,當執行非同步任務的時候微任務優先於巨集任務,本輪的迴圈執行結束後才會去上一層進行往下執行;
 

 

相關文章