一篇文章圖文並茂地帶你輕鬆學完 JavaScript 事件迴圈機制(event loop)

Huro~發表於2021-02-04

JavaScript 事件迴圈機制 (event loop)

本篇文章已經預設你有了基礎的 ES6javascript語法 知識。

本篇文章比較細緻,如果已經對同步非同步,單執行緒等概念比較熟悉的讀者可以直接閱讀執行棧後面的內容瞭解 event loop 原理

在瞭解 JavaScript 事件迴圈機制之前,得先了解同步與非同步的概念

同步與非同步

  1. 同步(Sync
const cal = () => {
    for (let i = 0; i < 1e8; i++) {
        // 做一些運算
    }
}

cal();
console.log("finish");

同步的含義是如果一個事情沒有做完,則不能執行下一個。

在這裡的例子如果 cal 函式沒有執行完畢 console.log 函式是不會執行的

對於 cal 稱為 同步函式。

  1. 非同步 (ASync)
$.ajax("xxx.com", function(res) {
    // ...
}); 
console.log("finish");

在上述程式碼中,$.ajax 的執行是非同步的,不會阻塞 console.log 的執行

即不必等到 $.ajax 請求返回資料後,才執行 console.log

對於 $.ajax 稱為非同步函式。

為什麼要有非同步函式?

單執行緒

javascript 是一門單執行緒語言,只能同時做一件事情。

如果沒有非同步函式,堵塞在程式的某個地方,會導致後面的函式得不到執行,瀏覽器作為使用者互動介面,顯然要能及時反映使用者的互動,因此要有非同步函式。

為什麼 javascript 不採用多執行緒呢?專門派發一個執行緒去處理使用者互動他不好嗎?

這個你可能得去問 javascript 的作者了。

執行棧

由於 javascript 是單執行緒語言,因此只有一個執行棧(呼叫棧)

function baz() {
    console.log("exec")
}

function bar() {
    baz();
}

function foo() {
    bar();
}

foo();

我們可以用一個動畫來演示執行棧的呼叫過程

根據動畫流程,我們詳細說一下呼叫棧的情況

  1. main 函式,也就是把整個 javascript 看成一個函式,入棧
  2. foo 函式被執行,入棧
  3. bar 函式被執行,入棧
  4. baz 函式被執行,入棧
  5. console.log 函式被執行,入棧
  6. console.log 函式執行完畢,出棧
  7. baz 函式執行完畢,出棧
  8. bar 函式執行完畢,出棧
  9. foo 函式執行完畢,出棧
  10. main 函式執行完畢,出棧

這種呼叫棧可以在程式報錯的時候起到很好的 debug 的作用

function baz() {
    throw new Error("noop!");
}

function bar() {
    baz();
}

function foo() {
    bar();
}

foo();

在檢視錯誤中,我們明顯的看到了之前提到的呼叫棧。

剛才的程式並無非同步函式,

如果我們在程式中用到了非同步函式

console.log("begin");

setTimeout(function cb(){
  console.log("finish")
}, 1000);

這個時候我們再看執行棧

進棧出棧過程類似上面的分析,可是在這裡,直到 main 函式執行完了,我們都沒看到 cb 函式執行,可是確確實實 1000ms 左右後 cb 函式真的執行了,這裡面是發生了什麼情況?

在解釋這個之前,我們先引入兩個概念

巨集觀任務和微觀任務

1. 巨集觀任務

ES5 之前,非同步操作由宿主發起,JavaScript 引擎並不能發起非同步操作,這類的非同步任務稱為巨集觀任務,比較典型的有

setTimeout(() => {
    console.log("exec")
}, 2000);

2.微觀任務

ES5 之後出現了 Promise ,用於解決回撥地獄的問題,這個函式也是非同步的,會等到 fulfill(resolve 或 reject) 後才會執行 then 方法

new Promise((resolve, reject) => {
    resolve("hello world")
}).then(data => {
    console.log(data)
})

這個非同步任務,由 v8 引擎發起 稱為微觀任務

這兩類任務對 event loop 也有影響

接下來進入本文章重點!!

event loop

event loop 分為瀏覽器環境和 node 環境,實現是不一樣的,本篇文章暫時只討論瀏覽器環境下的 event loop

1. 瀏覽器環境下的 event loop

接下來,我們具體看一個很大的例子

console.log("1");

setTimeout(function cb1(){
    console.log("2")
}, 0);

new Promise(function(resolve, reject) {
    console.log("3")
    resolve();
}).then(function cb2(){
    console.log("4");
})

console.log("5")

這段程式碼用 event loop 的解釋是這樣的

用文字解釋如下,上述動畫以及文字解釋忽略 main 函式

  1. console.log("1") 入棧出棧,控制檯顯示 1
  2. setTimeout 入棧,加入非同步任務佇列(此時處於等待執行完成的狀態,對於setTimeout來說就是等待延遲時間算執行完成,對於Promise 來說就是被 fulfill 了才算執行完成。
  3. new Promise 入棧出棧,控制檯顯示 3,並且把函式放入非同步佇列,等待完成了,就執行 then 方法,這裡的話,演示動畫忘記加上了。
  4. console.log(5) 入棧出棧,控制檯顯示 5

至此,主函式內的任務全部執行完畢,

這裡需要先知道,當任務放入非同步任務佇列後他們如果完成了,就會自動進入微觀任務或者巨集觀任務佇列。

這個時候 event loop 檢索微觀任務佇列是否有任務,如果有,就拖到 執行棧中執行,如果沒有的話,就檢索巨集觀任務佇列是否有任務。

而且,如果一旦微觀任務佇列有任務,就一定會先執行微觀任務佇列的。

如果一旦執行棧有任務就一定會先執行執行棧的。

可以用程式碼表述如下

while (true) {
    while (如果執行棧有任務) {
        // 執行
    }
    if (微觀任務佇列有任務) {
        // 執行
        continue;
    }
    if (巨集觀任務佇列有任務) {
        // 執行
        continue;
    }
}

至此,我們很容易得到上面的程式碼的執行結果是

"1", "3", "5", "4", "2"

在做一個巨集觀任務巢狀微觀任務的例子加深上述流程的理解。

console.log("1");

setTimeout(() => {
    console.log("2")
    new Promise(resolve => {
      resolve()
    }).then(() => {
      console.log("3")
    })
}, 0);

setTimeout(() => {
  console.log("4")
}, 0);

console.log("5")

執行結果會是

"1", "5", "2", "3", "4"

相關文章