🔥EventLoop是什麼?執行機制是什麼?


image.png

這是我參與更文挑戰的第20天,活動詳情查看: 更文挑戰javascript

前言

面試官:給我說說事件循環吧,就是EventLoop的機制,這個你據說過吧。html

我:...💥💥💥🚀java

什麼是 EventLoop ?

先看這張圖,先無論宏任務,微任務是什麼,先看整個流程。web

image-20210406150156448

分析:面試

  1. 判斷宏任務隊列是否爲空promise

    • 不空 --> 執行最先進入隊列的任務 --> 執行下一步
    • 空 --> 執行下一步
  2. 判斷微任務隊列是否爲空瀏覽器

    • 不空 --> 執行最先進入隊列的任務 --> 繼續檢查微任務隊列空不空
    • 空 --> 執行下一步

由於首次執行宏隊列中會有 script(總體代碼塊)任務,因此實際上就是 Js 解析完成後,在異步任務中,會先執行完全部的微任務,這裏也是不少面試題喜歡考察的。須要注意的是,新建立的微任務會當即進入微任務隊列排隊執行,不須要等待下一次輪迴。markdown

主線程從任務隊列讀取事件,這個過程是循環不斷的,因此整個運行機制又稱爲 Event Loop(事件循環)。數據結構

在深刻事件循環機制以前,須要弄懂一下幾個概念:異步

  • 執行上下文( Execution context )
  • 執行棧( Execution stack )
  • 微任務( micro-task )
  • 宏任務( macro-task )

執行上下文( Execution context )

執行上下文是一個抽象的概念,能夠理解爲是代碼執行的一個環境。JS 的執行上下文分爲三種,全局執 行上下文、函數(局部)執行上下文、Eval 執行上下文。

  • 全局執行上下文:全局執行上下文指的是全局 this 指向的 window ,能夠是外部加載的 JS 文件 或者本地 標籤中的代碼。
  • 函數執行上下文:函數上下文也稱爲局部上下文,每一個函數被調用的時候,都會建立一個新的局部 上下文。
  • Eval 執行上下文: 這個不經常使用,這裏不說。

執行棧( Execution stack )

執行棧,就是咱們數據結構中的「棧」,它具備「先進後出」的特色,正是由於這種特色,在咱們代碼進行 執行的時候,遇到一個執行上下文就將其依次壓入執行棧中。

當代碼執行的時候,先執行位於棧頂的執行上下文中的代碼,當棧頂的執行上下文代碼執行完畢就會出 棧,繼續執行下一個位於棧頂的執行上下文。

function foo() {
    console.log('a');
    bar();
    console.log('b');
}
function bar() {
    console.log('c');
}
foo();
複製代碼
  1. 初始化狀態,執行棧任務爲空。
  2. foo 函數執行, foo 進入執行棧,輸出 a ,碰到函數 bar 。
  3. 而後 bar 再進入執行棧,開始執行 bar 函數,輸出 c 。
  4. bar 函數執行完出棧,繼續執行執行棧頂端的函數 foo ,最後輸出 b 。
  5. foo 出棧,全部執行棧內任務執行完畢。

什麼是宏任務與微任務?

咱們都知道 Js 是單線程都,可是一些高耗時操做就帶來了進程阻塞問題。爲了解決這個問題,Js 有兩種任務的執行模式:同步模式(Synchronous)和異步模式(Asynchronous)

在異步模式下,建立異步任務主要分爲宏任務與微任務兩種。ES6 規範中,

  • 宏任務(Macrotask) 稱爲 Task, 微任務(Microtask) 稱爲 Jobs。
  • 宏任務是由宿主(瀏覽器、Node)發起的,而微任務由 JS 自身發起。
宏任務與微任務的幾種建立方式 👇
宏任務(Macrotask) 微任務(Microtask)
setTimeout requestAnimationFrame(有爭議)
setInterval MutationObserver(瀏覽器環境)
MessageChannel Promise.[ then/catch/finally ]
I/O,事件隊列 process.nextTick(Node環境)
setImmediate(Node環境) queueMicrotask
script(總體代碼塊)

注意:nextTick 隊列會比 Promie 隊列先執行。

如何理解 script(總體代碼塊)是個宏任務呢 🤔

實際上若是同時存在兩個 script 代碼塊,會首先在執行第一個 script 代碼塊中的同步代碼,若是這個過程當中建立了微任務並進入了微任務隊列,第一個 script 同步代碼執行完以後,會首先去清空微任務隊列,再去開啓第二個 script 代碼塊的執行。

因此這裏應該就能夠理解 script(總體代碼塊)爲何會是宏任務。

以上概念弄明白以後,再來看循環機制是如何運行的呢?如下涉及到的任務執行順序都是靠函數調用棧 來實現的。

  1. 首先,事件循環機制的是從script標籤內的代碼開始的,上邊咱們提到過,整個script標籤 做爲一個宏任務處理的。
  2. 在代碼執行的過程當中,若是遇到宏任務,如:setTimeout,就會將當前任務分發到對應的執行隊 列中去。
  3. 當執行過程當中,若是遇到微任務,如: Promise ,在建立 Promise 實例對象時,代碼順序執行,若是 到了執行· then 操做,該任務就會被分發到微任務隊列中去。
  4. script 標籤內的代碼執行完畢,同時執行過程當中所涉及到的宏任務也和微任務也分配到相應的隊 列中去。
  5. 此時宏任務執行完畢,而後去微任務隊列執行全部的存在的微任務。
  6. 微任務執行完畢,第一輪的消息循環執行完畢,頁面進行一次渲染。
  7. 而後開始第二輪的消息循環,從宏任務隊列中取出任務執行。
  8. 若是兩個任務隊列沒有任務可執行了,此時全部任務執行完畢。

定時器 setTimeout

任務隊列除此以外,還能夠放定時器的回調函數,須要指定某些代碼多少時間以後執行。

定時器主要包括兩種, setTimeout 和 setInterval 兩個函數。 當咱們設置定時器的時間,執行某個特定的任務,以下:

// 1 秒後執行
  setTimeout(function () {
    console.log(2);
  }, 1000);
  console.log(1)


複製代碼

上述的輸出結果爲 1, 2,執行完同步代碼後,就會執行定時器中的任務事件

// 同步執行完當即執行
  setTimeout(function () {
    console.log(2);
  }, 0);
  console.log(1)
複製代碼

當咱們執行 setTimeout(fn,0) 定時器時,會將這個定時任務回調放在任務隊列的尾部,表明的含 義就是儘早的執行。

也就是等到主線程同步任務和"任務隊列"現有的事件都處理完,而後纔會當即執行這個定時器的任務。

上述的前提是,等到同步任務和任務隊列的代碼執行完畢後,若是當前代碼執行很長時間,定時器並沒 辦法保證必定在指定時間執行。

注:HTML5 標準規定了setTimeout() 的第二個參數的最小值(最短間隔),不得低於4毫秒, 若是低於這個值,就會自動增長。

若是涉及到頁面的改動,這個定時器任務一般不會當即執行,而是 16 毫秒執行一次,咱們一般使用 requestAnimationFrame() 。

小實戰

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>消息運行機制</title>
</head>

<body>
  
</body>
<script> console.log('1'); setTimeout(() => { console.log('2') }, 1000); new Promise((resolve, reject) => { console.log('3'); resolve(); console.log('4'); }).then(() => { console.log('5'); }); console.log('6');// 1,3,4,6,5,2 </script>

</html>
複製代碼

分析:

  • 初始化狀態,執行棧爲空。
  • 首先執行<script>標籤內的同步代碼,此時全局的代碼進入執行棧中,同步順序執行代碼,輸出 1。
  • 執行過程當中遇到異步代碼 setTimeout (宏任務),將其分配到宏任務異步隊列中。
  • 同步代碼繼續執行,遇到一個 promise 異步代碼(微任務)。可是構造函數中的代碼爲同步代碼,依次輸出三、4,則then以後的任務加入到微任務隊列中去。
  • 最後執行同步代碼,輸出 6。
  • 由於 script 內的代碼做爲宏任務處理,因此這次循環進行處處理微任務隊列中的全部異步任務,直達微任務隊列中的全部任務執行完成爲止,微任務隊列中只有一個微任務,因此輸出 5。
  • 此時頁面要進行一次頁面渲染,渲染完成以後,進行下一次循環。
  • 在宏任務隊列中取出一個宏任務,也就是以前的 setTimeout,最後輸出 2。
  • 此時任務隊列爲空,執行棧中爲空,整個程序執行完畢。

image-20210406152314952

以上不免有些囉嗦,因此簡化整理以下步驟:

  • 一開始執行宏任務( script 中同步代碼),執行完畢,調用棧爲空。
  • 而後檢查微任務隊列是否有可執行任務,執行完全部微任務。
  • 進行頁面渲染。
  • 第二輪從宏任務隊列取出一個宏任務執行,重複以上循環。
相關文章
相關標籤/搜索