JavaScript 事件循環機制

前端開發的童鞋應該都知道,JavaScript 是一門單線程的腳本語言。這就意味着 JavaScript 代碼在執行的時候,只有一個主線程來執行全部的任務,同一個時間只能作同一件事情。html

那麼爲何 JavaScript 不設計成多線程的語言呢?前端

這是由其執行的環境是瀏覽器環境所決定的。試想一下若是 JavaScript 是多線程語言的話,那麼當兩個線程同時對 Dom 節點進行操做的時候,則可能會出現有歧義的問題,例如一個線程操做的是在一個 Dom 節點中添加內容,另外一個線程操做的是刪除該 Dom 節點,那麼應該以哪一個線程爲準呢?因此 JavaScript 做爲瀏覽器的腳本語言,其設計只能是單線程的。html5

須要注意的是,Html5 提出了 Web Worker,容許建立多個在後臺運行的子線程來執行 JavaScript 腳本。可是因爲子線程徹底受主線程控制,並且不可以干擾用戶界面(即不能操做 Dom),因此這並無改變 JavaScript 單線程的本質。node

上面講到,JavaScript 是一門單線程的腳本語言。所謂單線程,就是指全部的任務都須要排隊一個個執行,只有前一個任務執行完了才能夠執行後一個任務。這就形成了一個問題,若是前一個任務耗時過長,則會阻塞下一個任務的執行,在頁面上用戶的感知便會是瀏覽器卡死的現象。promise

而因爲在大部分的狀況中,形成任務耗時過長不是任務自己計算量大而致使 CPU 處理不過來,而是由於該任務須要與 IO 設備交互而致使的耗時過長,但這時 CPU 倒是處於閒置狀態的。因此爲了解決這個問題,便有了本章節的 JavaScript(也能夠說是瀏覽器的)事件循環(Event Loop)機制瀏覽器

在 JavaScript 事件循環機制中,使用到了三種數據對象,分別是棧(Stack)、堆(Heap)和隊列(Queue)。bash

  • 棧:一種後進先出(LIFO)的數據結構。能夠理解爲取乒乓球時的場景,後面放進去的乒乓球反而是最早取出來的。
  • 堆:一種樹狀的的數據結構。能夠理解爲在圖書館中取書的場景,能夠經過圖書索引的方式直接找到須要的書。
  • 隊列:一種先進先出(FIFO)的數據結構。即咱們平時排隊的場景,先排的人老是先出隊列。

在 JavaScript 事件循環機制中,使用的棧數據結構即是執行上下文棧,每當有函數被調用時,便會建立相對應的執行上下文並將其入棧;使用到堆數據結構主要是爲了表示一個大部分非結構化的內存區域存放對象;使用到的隊列數據結構即是任務隊列,主要用於存放異步任務。數據結構

棧、堆、隊列可視化表示

執行上下文棧

在 JavaScript 代碼運行過程當中,會進入到不一樣的執行環境中,一開始執行時最早進入到全局環境,此時全局上下文首先被建立併入棧,以後當調用函數時則進入相應的函數環境,此時相應函數上下文被建立併入棧,當處於棧頂的執行上下文代碼執行完畢後,則會將其出棧。這裏的棧即是執行上下文棧。多線程

舉個例子~異步

function fn2() {
    console.log('fn2')
}
function fn1() {
    console.log('fn1')
    fn2();
}

fn1();
複製代碼

上述代碼中的執行上下文棧變化行爲以下圖

執行上下文棧 ECStack

任務隊列

在 JavaScript 事件循環機制中,存在多種任務隊列,其分爲宏任務(macro-task)和微任務(micro-task)兩種。

  • 宏任務包括:setTimeoutsetIntervalI/OUI rendering
  • 微任務包括:PromiseObject.observe(已廢棄)MutationObserver(html5新特性)

上述所描述的 setTimeout、Promise 等都是指一種任務源,其對應一種任務隊列,真正放入任務隊列中的,是任務源指定的異步任務。在代碼執行過程當中,遇到上述任務源時,會將該任務源指定的異步任務放入不一樣的任務隊列中。

不一樣的任務源對應的任務隊列其執行順序優先級是不一樣的,上述宏任務和微任務的前後順序表明了其任務隊列執行順序的優先級。

即在宏任務隊列中,各個隊列的優先級爲 setTimeout > setInterval > I/O 在微任務隊列中,各個隊列的優先級爲 Promise > Object.observe > MutationObserver

對於 UI rendering 來講,瀏覽器會在每次清空微任務隊列會根據實際狀況觸發,這裏不作詳細贅述。

事件循環機制流程

  1. 主線程執行 JavaScript 總體代碼,造成執行上下文棧,當遇到各類任務源時將其所指定的異步任務掛起,接受到響應結果後將異步任務放入對應的任務隊列中,直到執行上下文棧只剩全局上下文;
  2. 將微任務隊列中的全部任務隊列按優先級、單個任務隊列的異步任務按先進先出(FIFO)的方式入棧並執行,直到清空全部的微任務隊列;
  3. 將宏任務隊列中優先級最高的任務隊列中的異步任務按先進先出(FIFO)的方式入棧並執行;
  4. 重複第 2 3 步驟,直到清空全部的宏任務隊列和微任務隊列,全局上下文出棧。

簡單來講,事件循環機制的流程就是,主線程執行 JavaScript 總體代碼後將遇到的各個任務源所指定的任務分發到各個任務隊列中,而後微任務隊列和宏任務隊列交替入棧執行直到清空全部的任務隊列,全局上下文出棧。

這裏要注意的是,任務源所指定的異步任務,並非當即被放入任務隊列中的,而是在接收到響應結果後纔會將其放入任務隊列中排隊。如 setTimeout 中指定延遲事件爲 1s,則在 1s 後纔會將該任務源所指定的任務隊列放入隊列中;I/O 交互只有接收到響應結果後纔將其異步任務放入隊列中排隊等待執行。

事件循環機制流程

是否是感受挺抽象的,舉個例子來實際感覺一下~

console.log('global');

setTimeout(function() {
    console.log('setTimeout1');
    new Promise(function(resolve) {
        console.log('setTimeout1_promise');
        resolve();
    }).then(function() {
        console.log('setTimeout1_promiseThen')
    })
    process.nextTick(function() {
        console.log('setTimeout1_nextTick');
    })
},0)

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promiseThen1')
})

setImmediate(function() {
    console.log('setImmediate');
})

process.nextTick(function() {
    console.log('nextTick');
})

new Promise(function(resolve) {
    console.log('promise2');
    resolve();
}).then(function() {
    console.log('promiseThen2')
})

setTimeout(function() {
    console.log('setTimeout2');
},0)
複製代碼

在這個例子中,主要分析在事件循環流程中各個任務隊列的變化狀況,對於執行上下文棧的行爲暫不作分析。任務隊列圖中左邊表明隊頭,右邊表明隊尾。

爲了可以實現該例子中有多個宏任務隊列和多個微任務隊列的狀況,我加入了 node 中的 setImmediate 和 process.nextTick ,node 中的事件循環機制與 JavaScript 相似,只是其實現機制有所不一樣,這裏咱們不須要關心。加入 node 兩個屬性後,其優先級以下

在宏任務隊列中,各個隊列的優先級爲 setTimeout > setInterval > setImmediate > I/O 微任務隊列中,各個隊列的優先級爲 process.nextTick > Promise > Object.observe > MutationObserver

因此上述例子只可以在 node 環境中執行,不可以在瀏覽器中執行。那麼讓咱們來一步步分析上述代碼的執行過程。

一,執行 Javascript 代碼,全局上下文入棧,輸出 global ,此時遇到第一個 setTimeout 任務源,因爲其執行延遲時間爲 0,因此可以當即接收到響應結果,將其指定的異步任務放入宏任務隊列中;

1

二,遇到第一個 Promise 任務源,此時會執行 Promise 第一個參數中的代碼,即輸出 promise1,而後將其指定的異步任務(then 中函數)放入微任務隊列中;

2

三,遇到 setImmediate 任務源,將其指定的異步任務放入宏任務隊列中;

3

四,遇到 nextTick 任務源,將其指定的異步任務放入微任務隊列中;

4

五,遇到第二個 Promise 任務源,輸出 promise2,將其指定的異步任務放入微任務隊列中;

5

六,遇到第二個 setTimeout 任務源,將其指定的異步任務放入宏任務隊列中;

6

七,JavaScript 總體代碼執行完畢,開始清空微任務隊列,將微任務隊列中的全部任務隊列按優先級、單個任務隊列的異步任務按先進先出的方式入棧並執行。此時咱們能夠看到微任務隊列中存在 Promise 和 nextTick 隊列,nextTick 隊列優先級比較高,取出 nextTick 異步任務入棧執行,輸出 nextTick;

7

八,取出 Promise1 異步任務入棧執行,輸出 promiseThen1;

8

九,取出 Promise2 異步任務入棧執行,輸出 promiseThen2;

9

十,微任務隊列清空完畢,執行宏任務隊列,將宏任務隊列中優先級最高的任務隊列中的異步任務按先進先出的方式入棧並執行。此時咱們能夠看到宏任務隊列中存在 setTimeout 和 setImmediate 隊列,setTimeout 隊列優先級比較高,取出 setTimeout1 異步任務入棧執行,輸出 setTimeout1,遇到 Promise 和 nextTick 任務源,輸出 setTimeout1_promise,將其指定的異步任務放入微任務隊列中;

10

十一,取出 setTimeout2 異步任務入棧執行,輸出 setTimeout2;

image.png

十二,至此一個微任務宏任務事件循環完畢,開始下一輪循環。從微任務隊列中的 nextTick 隊列取出 setTimeout1_nextTick 異步任務入棧執行,輸出 setTimeout1_nextTick;

12

十三,從微任務隊列中的 Promise 隊列取出 setTimeout1_promise 異步任務入棧執行,輸出 setTimeout1_promiseThen;

13

十四,從宏任務隊列中的 setImmediate 隊列取出 setImmediate 異步任務入棧執行,輸出 setImmediate;

14

十五,全局上下文出棧,代碼執行完畢。最終輸出結果爲

global
promise1
promise2
nextTick
promiseThen1
promiseThen2
setTimeout1
setTimeout1_promise
setTimeout2
setTimeout1_nextTick
setTimeout1_promiseThen
setImmediate
複製代碼

以爲還不錯的小夥伴,能夠關注一波公衆號哦。

相關文章
相關標籤/搜索