一篇文章圖文並茂地帶你輕鬆學完 JavaScript 事件迴圈機制(event loop)
阿新 • • 發佈:2021-02-05
## JavaScript 事件迴圈機制 (event loop)
本篇文章已經預設你有了基礎的 `ES6` 和 `javascript語法` 知識。
本篇文章比較細緻,如果已經對同步非同步,單執行緒等概念比較熟悉的讀者可以直接閱讀執行棧後面的內容瞭解 event loop 原理
在瞭解 `JavaScript` 事件迴圈機制之前,得先了解同步與非同步的概念
### 同步與非同步
1. 同步(Sync
```js
const cal = () => {
for (let i = 0; i < 1e8; i++) {
// 做一些運算
}
}
cal();
console.log("finish");
```
同步的含義是如果一個事情沒有做完,則不能執行下一個。
在這裡的例子如果 `cal` 函式沒有執行完畢 `console.log` 函式是不會執行的
對於 `cal` 稱為 同步函式。
2. 非同步 (ASync)
```js
$.ajax("xxx.com", function(res) {
// ...
});
console.log("finish");
```
在上述程式碼中,`$.ajax` 的執行是非同步的,不會阻塞 `console.log` 的執行
即不必等到 `$.ajax` 請求返回資料後,才執行 `console.log`
對於 `$.ajax` 稱為非同步函式。
為什麼要有非同步函式?
### 單執行緒
`javascript` 是一門單執行緒語言,只能同時做一件事情。
如果沒有非同步函式,堵塞在程式的某個地方,會導致後面的函式得不到執行,瀏覽器作為使用者互動介面,顯然要能及時反映使用者的互動,因此要有非同步函式。
為什麼 `javascript` 不採用多執行緒呢?專門派發一個執行緒去處理使用者互動他不好嗎?
這個你可能得去問 `javascript` 的作者了。
### 執行棧
由於 `javascript` 是單執行緒語言,因此只有一個執行棧(呼叫棧)
```js
function baz() {
console.log("exec")
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
```
我們可以用一個動畫來演示執行棧的呼叫過程
![](https://img2020.cnblogs.com/blog/2286610/202102/2286610-20210204215617528-1282621131.gif)
根據動畫流程,我們詳細說一下呼叫棧的情況
1. `main` 函式,也就是把整個 `javascript` 看成一個函式,入棧
2. `foo` 函式被執行,入棧
3. `bar` 函式被執行,入棧
4. `baz` 函式被執行,入棧
5. `console.log` 函式被執行,入棧
6. `console.log` 函式執行完畢,出棧
7. `baz` 函式執行完畢,出棧
8. `bar` 函式執行完畢,出棧
9. `foo` 函式執行完畢,出棧
10. `main` 函式執行完畢,出棧
這種呼叫棧可以在程式報錯的時候起到很好的 `debug` 的作用
```js
function baz() {
throw new Error("noop!");
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
```
![](https://img2020.cnblogs.com/blog/2286610/202102/2286610-20210204215628004-1796198631.png)
在檢視錯誤中,我們明顯的看到了之前提到的呼叫棧。
剛才的程式並無非同步函式,
如果我們在程式中用到了非同步函式
```js
console.log("begin");
setTimeout(function cb(){
console.log("finish")
}, 1000);
```
這個時候我們再看執行棧
![](https://img2020.cnblogs.com/blog/2286610/202102/2286610-20210204215635269-303715261.gif)
進棧出棧過程類似上面的分析,可是在這裡,直到 `main` 函式執行完了,我們都沒看到 `cb` 函式執行,可是確確實實 `1000ms` 左右後 `cb` 函式真的執行了,這裡面是發生了什麼情況?
在解釋這個之前,我們先引入兩個概念
### 巨集觀任務和微觀任務
#### 1. 巨集觀任務
在 `ES5` 之前,非同步操作由宿主發起,`JavaScript` 引擎並不能發起非同步操作,這類的非同步任務稱為巨集觀任務,比較典型的有
```js
setTimeout(() => {
console.log("exec")
}, 2000);
```
#### 2.微觀任務
在 `ES5` 之後出現了 `Promise` ,用於解決回撥地獄的問題,這個函式也是非同步的,會等到 `fulfill(resolve 或 reject)` 後才會執行 `then` 方法
```js
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`
接下來,我們具體看一個很大的例子
```js
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` 的解釋是這樣的
![](https://img2020.cnblogs.com/blog/2286610/202102/2286610-20210204215645929-1357278464.gif)
用文字解釋如下,上述動畫以及文字解釋忽略 `main` 函式
1. `console.log("1")` 入棧出棧,控制檯顯示 `1`
2. `setTimeout` 入棧,加入非同步任務佇列(此時處於等待執行完成的狀態,對於setTimeout來說就是等待延遲時間算執行完成,對於`Promise` 來說就是被 `fulfill` 了才算執行完成。
3. `new Promise` 入棧出棧,控制檯顯示 `3`,並且把函式放入非同步佇列,等待完成了,就執行 `then` 方法,這裡的話,演示動畫忘記加上了。
4. `console.log(5)` 入棧出棧,控制檯顯示 `5`
至此,主函式內的任務全部執行完畢,
這裡需要先知道,當任務放入非同步任務佇列後他們如果完成了,就會自動進入微觀任務或者巨集觀任務佇列。
這個時候 `event loop` 檢索微觀任務佇列是否有任務,如果有,就拖到 執行棧中執行,如果沒有的話,就檢索巨集觀任務佇列是否有任務。
而且,如果一旦微觀任務佇列有任務,就一定會先執行微觀任務佇列的。
如果一旦執行棧有任務就一定會先執行執行棧的。
可以用程式碼表述如下
```js
while (true) {
while (如果執行棧有任務) {
// 執行
}
if (微觀任務佇列有任務) {
// 執行
continue;
}
if (巨集觀任務佇列有任務) {
// 執行
continue;
}
}
```
至此,我們很容易得到上面的程式碼的執行結果是
```js
"1", "3", "5", "4", "2"
```
在做一個巨集觀任務巢狀微觀任務的例子加深上述流程的理解。
```js
console.log("1");
setTimeout(() => {
console.log("2")
new Promise(resolve => {
resolve()
}).then(() => {
console.log("3")
})
}, 0);
setTimeout(() => {
console.log("4")
}, 0);
console.log("5")
```
執行結果會是
```js
"1", "5", "2", "3",