1. 程式人生 > >javascript的執行機制—Event Loop

javascript的執行機制—Event Loop

如果 異步任務。 兩個 用戶 徹底 再次 IT bsp 執行順序

既然今天要談的是javascript的事件循環機制,要理解事件循環,首先要知道事件循環是什麽。

我們先從一個例子來看一下javascript的執行順序。

<script>
    setTimeout(function() {
        console.log(‘定時器開始了.‘);
    },0)
    
    new Promise(function(resolve) {
        console.log(‘馬上執行for循環了‘);
        for (let i = 0; i < 10000; i++) {
            i == 99 && resolve();
        }
    }).then(
function() { console.log(‘執行then函數了‘); }) console.log(‘代碼執行結束‘); //執行結果為: //馬上執行for循環了 //代碼執行結束 //執行then函數了 //定時器開始了. </script>

怎麽樣,是不是和自己在心裏運行的結果差了一萬八千裏呢。如果是的話,請耐心看完後面的內容,讓你徹底弄明白javascript的事件循環機制。

單線程的javascript

要想了解事件循環的我們就得從javascript的工作原理開始說起。

javascript語言的一大特點就是單線程,可是為什麽javascript不做成多線程呢?

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?

任務隊列

我們說單線程就意味著所有的任務必須排隊。就類似於銀行只有一個窗口,前一個任務執行結束後,後一個任務才能執行。如果新執行的任務耗時很長,那麽後一個任務就不得不一直等著。

這樣就又出現了一個問題,在進行瀏覽器的操作時,我們常常會通過ajax向後臺發送請求,然而js必須等到瀏覽器接收到響應內容後才會繼續往下執行,如果這段時間是10s,那麽頁面必須停在這裏10s。這不僅會影響用戶體驗,也會降低CPU的利用率

,顯然不是我們想要的。

於是,聰明的程序員小哥哥就把任務分成了兩類

  • 同步任務
  • 異步任務

同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;

異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行,通常來說擁有callback回調函數的任務就是異步任務。

同步任務和異步任務的執行過程大致可以簡化成如下的導圖所示。

技術分享圖片

  • 同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。
  • 主線程內的任務執行完畢為空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重復,也就是常說的Event Loop(事件循環)。
為了便於理解事件循環,我們來看一段代碼。
<script>
    console.log(1);
    setTimeout(function task() { 
    console.log(‘定時器執行了.‘);
  },
1000);
  console.log(
2); </script>
  • js代碼從上往下依次執行,
  • 遇到console.log(1),執行並打印出來。.
  • 遇到異步任務setTimeout,task進入Event Table並註冊,計時開始。
  • 遇到console.log(2),執行並打印出來。
  • 主線程執行完畢,開始查詢任務隊列有沒有等待執行的回調函數。
  • 一秒鐘到後,timeout計時事件完成,task進入Event Queue。
  • 主線程發現任務隊列有等待執行的函數task,將task調進進入主線程執行。

我們不禁要問了,那怎麽知道主線程執行棧為空啊?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否為空,一旦為空,就會去Event Queue那裏檢查是否有等待被調用的函數。

macrotask 與 microtask

其實除了廣義的同步任務和異步任務的劃分,異步任務還可以細分為宏任務(macrotask) 與微任務( microtask)。不同的異步任務類型會進入不同的Event Queue。

宏任務: 需要多次事件循環才能執行完,事件隊列中的每一個事件都是一個宏任務。每次事件循環都會調入一個宏任務,瀏覽器為了能夠使得js內部宏任務與DOM任務有序的執行,會在一個宏任務執行結束後,在下一個宏執行開始前,對頁面進行重新渲染 (task->渲染->task->…)。例如鼠標點擊會觸發一個事件回調,需要執行一個宏任務,然後解析HTML。setTimeout的作用是等待給定的時間後為它的回調產生一個新的宏任務。

微任務: 微任務是一次性執行完的。微任務通常來說是需要在當前task執行結束後立即執行的任務,例如對一些動作做出反饋或者異步執行任務又不需要分配一個新的task,這樣便可以提高一些性能。只要執行棧中沒有其他的js代碼正在執行了,而且當前調入的宏任務都執行完了,微任務隊列會立即執行。如果在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,之後也會被執行。

簡單理解,宏任務在下一輪事件循環執行,微任務在本輪事件循環的所有任務結束後執行

  • 宏任務主要包括了:setTimeout、setInterval、setImmediate、I/O、各種事件(比如鼠標單擊事件)的回調函數
  • 優先級:主代碼塊 > setImmediate > MessageChannel > setTimeout / setInterval
  • 微任務主要包括了:process.nextTick、Promise、MutationObserver
  • 優先級:process.nextTick > Promise > MutationObserver

據whatwg規範介紹:

  • 一個事件循環(event loop)會有一個或多個任務隊列(task queue)
  • 每一個 event loop 都有一個 microtask queue
  • task queue == macrotask queue != microtask queue
  • 一個任務 task 可以放入 macrotask queue 也可以放入 microtask queue 中
  • 調用棧清空(只剩全局),然後執行所有的microtask。當所有可執行的microtask執行完畢之後。循環再次從macrotask開始,找到其中一個宏任務執行完畢,如果這個宏任務中可能包含宏任務或微任務,會將宏任務添加到事件隊列中,然後再執行所有的microtask,這樣一直循環下去。

宏任務、微任務執行流程圖如下所示。

技術分享圖片

這時,我們再來看一下文章開頭給的一段代碼。

<script>
    setTimeout(function() {
        console.log(‘定時器開始了.‘);
    },0)
    
    new Promise(function(resolve) {
        console.log(‘馬上執行for循環了‘);
        for (let i = 0; i < 10000; i++) {
            i == 99 && resolve();
        }
    }).then(function() {
        console.log(‘執行then函數了‘);
    })
    
    console.log(‘代碼執行結束‘);
    //執行結果為:
    //馬上執行for循環了
    //代碼執行結束
    //執行then函數了
    //定時器開始了.
</script>

執行步驟如下所示。

  • 當頁面首次加載時,<script>標簽內的代碼段作為一個宏任務進入主線程,依次向下執行。
  • 遇到setTimeout將回調函數註冊後壓入宏任務的事件隊列。
  • 遇到new Promise立即執行,輸出‘馬上執行for循環了‘。將then函數壓入到微任務隊列。
  • 遇到console.log(‘代碼執行結束‘),執行代碼。輸出"代碼執行結束了"。
  • 主線程執行完後,先檢查微任務隊列中有沒有待執行的任務,發現then函數在微任務隊列裏,將其取出到主線程執行,輸出"執行then函數了"。
  • 開始下一輪事件循環,從紅任務隊列中取出setTimeout事件的回調函數,執行。輸出“定時器開始了”。
  • 結束。

總結

javascript是一門單線程的語言,事件循環是js異步編程的一種方法。也是js的執行機制。當瀏覽器中的網頁剛剛載入的時候,<script>裏的代碼會作為第一個宏任務被壓入棧執行,同步代碼執行完後,如果有微任務就執行微任務,沒有微任務就執行下一個宏任務。如此往復循環,直至所有任務都執行完畢。

參考文章

JavaScript 運行機制詳解:再談Event Loop

瀏覽器內的事件隊列

這一次,徹底弄懂 JavaScript 執行機制

javascript的執行機制—Event Loop