JavaScript事件循環機制

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 “
 
  總結:瀏覽器在執行代碼的時候碰到同步任務會一步一步往下執行,碰到異步任務會放入到任務隊列中等同步任務執行完以後再放入到主進程中執行;所以每一輪事件循環的執行順序應該都是同步優先異步執行,異步又分為宏任務和微任務,當執行異步任務的時候微任務優先於宏任務,本輪的循環執行結束後才會去上一層進行往下執行;