筆試題——JavaScript事件迴圈機制(event loop、macrotask、microtask)

立志搬磚造福生活發表於2019-03-04

今天做了一道筆試題覺得很有意義分享給大家,題目如下:

setTimeout(()=>{
    console.log(`A`);
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log(`B`)
        },0);
        return new Promise(function (resolve) {
            console.log(`C`);
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log(`D`)
});
console.log(`E`);複製程式碼

JavaScript 都知道它是一門單執行緒的語言,這也就意味著 JS 無法進行多執行緒程式設計,但是 JS 當中卻有著無處不在的非同步概念 。要完全理解非同步,就需要了解 JS 的執行核心——事件迴圈(event loop)。

 一、什麼是事件佇列?

首先來看一個小小的demo

console.log(`start`);
setTimeout(()=>{
    console.log(`A`);
},1000);
console.log(`end`);
//start
//end
//A複製程式碼

js執行之後,程式輸出 `start` 和 `end`,在大約1s之後輸出了 `A` 。那我們就有疑問了?為什麼A不在end之前執行呢?

這是因為 setTimeout 是一個非同步的函式。意思也就是說當我們設定一個延遲函式的時候,當前指令碼並不會阻塞,它只是會在瀏覽器的事件表中進行記錄,程式會繼續向下執行。當延遲的時間結束之後,事件表會將回撥函式新增至事件佇列(task queue)中,事件佇列拿到了任務過後便將任務壓入執行棧(stack)當中,執行棧執行任務,輸出 `A`。

事件佇列是一個儲存著待執行任務的佇列,其中的任務嚴格按照時間先後順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最後執行。事件佇列每次僅執行一個任務,在該任務執行完畢之後,再執行下一個任務。執行棧則是一個類似於函式呼叫棧的執行容器,當執行棧為空時,JS 引擎便檢查事件佇列,如果不為空的話,事件佇列便將第一個任務壓入執行棧中執行。

那麼我將這個例子做一個小小的改動看一看:

console.log(`start`);
setTimeout(()=>{
    console.log(`A`);
},0);
console.log(`end`);
//start
//end
//A複製程式碼

可以看出,我們將settimeout第二個引數設定為0後,`A` 也總是會在 `end` 之後輸出。所以究竟發生了什麼?這是因為 setTimeout 的回撥函式只是會被新增至事件佇列,而不是立即執行。由於當前的任務沒有執行結束,所以 setTimeout 任務不會執行,直到輸出了 `end` 之後,當前任務執行完畢,執行棧為空,這時事件佇列才會把 setTimeout 回撥函式壓入執行棧執行。

 二、Promise的含義和基本用法?

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

寫一個小demo看一下Promise的執行機制:

let promise = new Promise(function(resolve, reject) {
  console.log(`Promise`);
  resolve();
});

promise.then(function() {
  console.log(`resolved.`);
});

console.log(`Hi!`);
// Promise
// Hi!
// resolved複製程式碼

上面程式碼中,Promise 新建後立即執行,所以首先輸出的是Promise。然後,then方法指定的回撥函式,將在當前指令碼所有同步任務執行完才會執行,所以resolved最後輸出。

 三、Macrotasks和Microtasks

Macrotasks和Microtasks 都屬於上述的非同步任務中的一種,他們分別有如下API:
macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promise, MutationObserver

setTimeout的macrotask, 和 Promise的microtask 有哪些不同,先來看下程式碼如下:

console.log(1);
setTimeout(function(){
  console.log(2);
}, 0);
Promise.resolve().then(function(){
  console.log(3);
}).then(function(){
  console.log(4);
});

//1
//3
//4
//2複製程式碼

如上程式碼可以看到,Promise的函式程式碼的非同步任務會優先setTimeout的延時為0的任務先執行。

原因是任務佇列分為 macrotasks 和 microtasks, 而promise中的then方法的函式會被推入到microtasks佇列中,而setTimeout函式會被推入到macrotasks

任務佇列中,在每一次事件迴圈中,macrotask只會提取一個執行,而microtask一直提取,直到microsoft佇列為空為止。

也就是說如果某個microtask任務被推入到執行中,那麼當主執行緒任務執行完成後,會迴圈呼叫該佇列任務中的下一個任務來執行,直到該任務佇列到最後一個任務為止。

而事件迴圈每次只會入棧一個macrotask,主執行緒執行完成該任務後又會檢查microtasks佇列並完成裡面的所有任務後再執行macrotask的任務。

 四、分析本題目

setTimeout(()=>{
    console.log(`A`);
},0);
var obj={
    func:function () {
        setTimeout(function () {
            console.log(`B`)
        },0);
        return new Promise(function (resolve) {
            console.log(`C`);
            resolve();
        })
    }
};
obj.func().then(function () {
    console.log(`D`)
});
console.log(`E`);複製程式碼

1、首先 setTimeout A 被加入到事件佇列中  ==>  此時macrotasks中有[‘A’];

2、obj.func()執行時,setTimeout B 被加入到事件佇列中  ==> 此時macrotasks中有[‘A’,‘B’];

3、接著return一個Promise物件,Promise 新建後立即執行 執行console.log(`C`); 控制檯首次列印‘C’;

4、然後,then方法指定的回撥函式,被加入到microtasks佇列,將在當前指令碼所有同步任務執行完才會執行。 ==> 此時microtasks中有[‘D’];

5、然後繼續執行當前指令碼的同步任務,故控制檯第二次輸出‘E’;

6、此時所有同步任務執行完畢,如上所述先檢查microtasks佇列完成其中所有任務,故控制檯第三次輸出‘D’;

7、最後再執行macrotask的任務,並且按照入佇列的時間順序,控制檯第四次輸出‘A’,控制檯第五次輸出‘B’。

 五、執行js程式碼

筆試題——JavaScript事件迴圈機制(event loop、macrotask、microtask)

分析與實際符合,NICE!

參考文章:www.cnblogs.com/tugenhua070…

還有阮老師的promise介紹:es6.ruanyifeng.com/?search=pro…

文章本人原創,轉載請評論;

前端菜鳥對JavaScript的理解還有很多不足,如有錯誤歡迎大家指出來;

喜歡的點個贊把!

相關文章