什麼是瀏覽器的Event Loop?,線程,循環特質

Event Loop是什麼

什麼是瀏覽器的Event Loop?,線程,循環特質

因為js設計之初,多線程的執行模式還不流行,所以一直以為,js都是單線程執行的。但是js擁有異步執行的能力,這依賴於事件循環(Event Loop)的執行模式。我們將通過js在瀏覽器中的執行來研究一下該模式。

其中涉及到一些概念,我們先簡單研究一下,以便後續更好地瞭解。

進程和線程

參考阮一峰的解釋,將整個CPU比喻為一座工廠,進程就是其中的車間,車間中的需要完成的工序就是線程。一個工廠可以有多個車間,每個車間有一個或者多個工序,但是必須 按照順序執行,這就是單線程的概念。也是瀏覽器事件執行的基礎。

瀏覽器渲染過程

瀏覽器是一個多進程應用,每一個窗口就是一個進程,其中包含以下線程:

  • GUI渲染線程

負責渲染頁面,佈局和繪製

頁面需要重繪和迴流時,該線程就會執行

與js引擎線程互斥,防止渲染結果不可預期

  • JS引擎線程

負責處理解析和執行javascript腳本程序

只有一個JS引擎線程(單線程)

與GUI渲染線程互斥,防止渲染結果不可預期

  • 事件觸發線程

用來控制事件循環(鼠標點擊、setTimeout、ajax等)

當事件滿足觸發條件時,將事件放入到JS引擎所在的執行隊列中

  • 定時觸發器線程

setInterval與setTimeout所在的線程

定時任務並不是由JS引擎計時的,是由定時觸發線程來計時的

計時完畢後,通知事件觸發線程

  • 異步http請求線程

瀏覽器有一個單獨的線程用於處理AJAX請求

當請求完成時,若有回調函數,通知事件觸發線程

各個進程之間的關係

  • 同步任務都在js引擎線程上完成,當前的任務都存儲在執行棧中;
  • js引擎線程執行到setTimeout/setInterval的時候,通知定時觸發器線程,間隔一定時間,觸發回調函數;
  • 定時觸發器線程在接收到這個消息後,會在等待的時間後,將回調事件放入到由事件觸發線程所管理的事件隊列(事件隊列分為宏任務隊列微任務隊列)中;
  • js引擎線程執行到XHR/fetch時,通知 異步http請求線程,發送一個網絡請求;
  • 異步http請求線程在請求成功後,將回調事件放入到由事件觸發線程事件隊列中;
  • 如果JS引擎線程中的執行棧沒有任務了,JS引擎線程會詢問事件觸發線程,在 事件隊列中是否有待執行的回調函數,如果有就會加入到執行棧中交給JS引擎線程執行;
  • JS引擎線程空閒之後,GUI渲染線程開始工作
什麼是瀏覽器的Event Loop?,線程,循環特質

總結:

  • JS 是可以操作 DOM 的, 因此瀏覽器設定 GUI渲染線程和 JS引擎線程為互斥關係;
  • setTimeout/setInterval和 XHR/fetch代碼執行時, 本身是同步任務,而其中的回調函數才是異步任務
  • JS引擎線程只執行執行棧中的事件
  • 執行棧中的代碼執行完畢,就會讀取事件隊列中的事件
  • 事件隊列中的回調事件,是由各自線程插入到事件隊列中的
  • 如此循環

js如何異步執行

瞭解了瀏覽器多線程之間的關聯之後,我們開始探究,js是如何依賴Event Loop,進行異步操作的。

執行棧和事件隊列

在分析多線程之間的關係時,我們提到了兩個概念,執行棧執行隊列

執行棧

棧,是一種數據結構,具有先進後出的原則。JS 中的執行棧就具有這樣的結構,當引擎第一次遇到 JS 代碼時,會產生一個全局執行上下文並壓入執行棧,每遇到一個函數調用,就會往棧中壓入一個新的上下文。引擎執行棧頂的函數,執行完畢,彈出當前執行上下文

事件隊列

事件隊列是一個存儲著 異步任務 的隊列,按照先進先出的原則執行。事件隊列每次僅執行一個任務。當執行棧為空時,JS 引擎便檢查事件隊列,如果事件隊列不為空的話,事件隊列便將第一個任務壓入執行棧中運行。

宏任務和微任務

異步任務又分為宏任務跟微任務、他們之間的區別主要是執行順序的不同。

宏任務(macrotask)

也叫tasks,一些異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:

  • 包括整體代碼script
  • setTimeout
  • setInterval
  • requestAnimationFrame

微任務(microtask)

也叫jobs,另一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:

  • Promise
  • MutationObserver
什麼是瀏覽器的Event Loop?,線程,循環特質

理解Event Loop

示例

下面上一道很經典的題目:

 console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');

沒有研究event loop之前,答案很可能以為是1 3 4 2,但是實際答案是1 4 3 2。其中的原理下面來分析一下。

異步任務執行的時候,有這樣一個順序:

  1. 執行全局Script代碼,如果碰到異步任務,將該任務放入微任務隊列中
  2. 全局Script執行完,執行棧清空
  3. 從微隊列microtask queue中取出位於隊首的回調任務,放入調用棧Stack中執行
  4. 一個微任務執行完畢之後,再從微任務隊列中取出一個任務放入執行棧執行,若微任務中還有微任務,則放入當前微任務隊列末尾
  5. 微任務隊列為空,執行棧也為空,此時從宏任務隊列取出一個任務執行,如果其中有微任務,放入微任務隊列
  6. 重複執行3-5步驟...
  7. 重複執行3-5步驟...

*注:

  1. 宏任務隊列一次只從隊列中取一個任務執行,執行完後就去執行微任務隊列中的任務;
  2. 微任務隊列中所有的任務都會被依次取出來執行,直到隊列為空;
  3. GUI渲染線程在微任務執行完,執行棧為空,下一個宏任務執行之前執行,。

以上就是瀏覽器事件循環——event loop。

理解了異步任務的執行順序之後,再來回顧上面這道題:

 console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');
  1. 執行整個script,console.log('1')是同步任務,setTimeout是宏任務,js引擎線程通知事件觸發線程
    ,在定時n秒後存入宏任務隊列中,所以先存入console.log('3'),後存入console.log('2');;
  2. 執行下一個同步任務console.log('4');;
  3. 執行棧為空,查詢微任務隊列也為空,查詢宏任務隊列
  4. 根據先進先出原則,執行console.log('3');,後執行console.log('2');
  5. 輸出1 4 3 2;

實踐

再來2道題鞏固一下:

一.

 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'];
  2. 接著執行obj的func方法,將setTimeout放到宏任務隊列,此時宏任務隊列為['A', 'B']
  3. 函數返回一個Promise,因為這是一個同步操作,所以先打印出'C';
  4. 接著將then放到微任務隊列,此時微任務隊列為 ['D'];
  5. 接著執行同步任務console.log('E');,打印出 'E';
  6. 因為微任務優先執行,所以先輸出 'D';
  7. 最後依次輸出 ['A', 'B'];
  8. 輸出結果:C E D A B

二.

 async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

await前面的代碼是同步的,調用此函數時會直接執行;而await a(); 這句可以被轉換成 Promise.resolve(a()); await 後面的代碼 則會被放到 Promise.then() 方法裡。因此上面的代碼可以被轉換成如下形式:

 function async1() {
console.log('async1 start'); // 2

Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
}

function async2() {
console.log('async2'); // 3
}

console.log('script start'); // 1

setTimeout(function() {
console.log('settimeout'); // 8
}, 0);

async1();

new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
  1. 首先打印出script start
  2. 接著將settimeout添加到宏任務隊列,此時宏任務隊列為['settimeout']
  3. 然後執行函數async1,先打印出async1 start,又因為Promise.resolve(async2()) 是同步任務,所以打印出async2,接著將async1 end 添加到微任務隊列,此時微任務隊列為['async1 end']
  4. 接著打印出promise1,將promise2 添加到微任務隊列,此時微任務隊列為['async1 end', promise2]
  5. 打印出script end
  6. 因為微任務優先級高於宏任務,所以先依次打印出 async1 end和promise2
  7. 最後打印出宏任務settimeout

我自己是一名從事了多年的前端老程序員,小編為大家準備了新出的前端編程學習資料,免費分享給大家!

如果你也想學習前端,那麼幫忙轉發一下然後再關注小編後私信【1】可以得到我整理的這些前端資料了(私信方法:點擊我頭像進我主頁有個上面有個私信按鈕)


分享到:


相關文章: