一篇文章圖文並茂地帶你輕松學完 JavaScript 事件循環機制(event loop)


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 loopnode 環境下的 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"

2. Node環境下的 event loop

請看 node官網對event loop的解釋


免責聲明!

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



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