JavaScript 事件迴圈機制 (event loop)
本篇文章已經預設你有了基礎的 ES6
和 javascript語法
知識。
本篇文章比較細緻,如果已經對同步非同步,單執行緒等概念比較熟悉的讀者可以直接閱讀執行棧後面的內容瞭解 event loop 原理
在瞭解 JavaScript
事件迴圈機制之前,得先了解同步與非同步的概念
同步與非同步
- 同步(Sync
const cal = () => {
for (let i = 0; i < 1e8; i++) {
// 做一些運算
}
}
cal();
console.log("finish");
同步的含義是如果一個事情沒有做完,則不能執行下一個。
在這裡的例子如果 cal
函式沒有執行完畢 console.log
函式是不會執行的
對於 cal
稱為 同步函式。
- 非同步 (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();
我們可以用一個動畫來演示執行棧的呼叫過程
根據動畫流程,我們詳細說一下呼叫棧的情況
main
函式,也就是把整個javascript
看成一個函式,入棧foo
函式被執行,入棧bar
函式被執行,入棧baz
函式被執行,入棧console.log
函式被執行,入棧console.log
函式執行完畢,出棧baz
函式執行完畢,出棧bar
函式執行完畢,出棧foo
函式執行完畢,出棧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
函式
console.log("1")
入棧出棧,控制檯顯示1
setTimeout
入棧,加入非同步任務佇列(此時處於等待執行完成的狀態,對於setTimeout來說就是等待延遲時間算執行完成,對於Promise
來說就是被fulfill
了才算執行完成。new Promise
入棧出棧,控制檯顯示3
,並且把函式放入非同步佇列,等待完成了,就執行then
方法,這裡的話,演示動畫忘記加上了。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"