事件循環
JavaScript 語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。為了協調事件、用戶交互、腳本、UI 渲染和網絡處理等行為,防止主線程的不阻塞,Event Loop 的方案應用而生。Event Loop 包含兩類:一類是基于 Browsing Context,一種是基于 Worker。二者的運行是獨立的,也就是說,每一個 JavaScript 運行的"線程環境"都有一個獨立的 Event Loop,每一個 Web Worker 也有一個獨立的 Event Loop。
本文所涉及到的事件循環是基于 Browsing Context。
任務隊列
根據規范,事件循環是通過任務隊列的機制來進行協調的。一個 Event Loop 中,可以有一個或者多個任務隊列(task queue),一個任務隊列便是一系列有序任務(task)的集合;每個任務都有一個任務源(task source),源自同一個任務源的 task 必須放到同一個任務隊列,從不同源來的則被添加到不同隊列。setTimeout/Promise
等API便是任務源,而進入任務隊列的是他們指定的具體執行任務。
在事件循環中,每進行一次循環操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的,但關鍵步驟如下:
在此次 tick 中選擇最先進入隊列的任務(oldest task),如果有則執行(一次)
檢查是否存在 Microtasks,如果存在則不停地執行,直至清空 Microtasks Queue
更新 render
主線程重復執行上述步驟
在上訴tick的基礎上需要了解幾點:
JS分為同步任務和異步任務
同步任務都在主線程上執行,形成一個執行棧
主線程之外,事件觸發線程管理著一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
一旦執行棧中的所有同步任務執行完畢(此時JS引擎空閑),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。
宏任務
(macro)task,可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)。
瀏覽器為了能夠使得JS內部(macro)task與DOM任務能夠有序的執行,會在一個(macro)task執行結束后,在下一個(macro)task 執行開始前,對頁面進行重新渲染,流程如下:
(macro)task->渲染->(macro)task->…
宏任務包含:
script(整體代碼)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 環境)
微任務
microtask,可以理解是在當前 task 執行結束后立即執行的任務。也就是說,在當前task任務后,下一個task之前,在渲染之前。
所以它的響應速度相比setTimeout
(setTimeout是task)會更快,因為無需等渲染。也就是說,在某一個macrotask執行完后,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)。
微任務包含:
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 環境)
運行機制
在事件循環中,每進行一次循環操作稱為 tick,每一次 tick 的任務處理模型是比較復雜的,但關鍵步驟如下:
執行一個宏任務(棧中沒有就從事件隊列中獲取)
執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)
當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
如圖:
實例講解
實例1
setTimeout(function(){console.log('定時器開始啦')});new Promise(function(resolve){console.log('馬上執行for循環啦');for(var i = 0; i < 10000; i++){i == 99 && resolve();}}).then(function(){console.log('執行then函數啦')});console.log('代碼執行結束');//馬上執行for循環啦 //代碼執行結束 //執行then函數啦 //定時器開始啦
首先執行script
下的宏任務,遇到setTimeout
,將其放到宏任務的【隊列】里
遇到 new Promise
直接執行,打印"馬上執行for循環啦"
遇到then
方法,是微任務,將其放到微任務的【隊列里】
打印 “代碼執行結束”
本輪宏任務執行完畢,查看本輪的微任務,發現有一個then方法里的函數, 打印"執行then函數啦"
到此,本輪的event loop 全部完成。
下一輪的循環里,先執行一個宏任務,發現宏任務的【隊列】里有一個 setTimeout
里的函數,執行打印"定時器開始啦"
實例2
async function async1() {console.log( 'async1 start' )await async2()console.log( 'async1 end' )
}
async function async2() {console.log( 'async2' )
}
async1()
console.log( 'script start' )//async1 start
//async2
//script start
//async1 end
一旦遇到 await
就立刻讓出線程,阻塞后面的代碼
等候之后,對于 await
來說分兩種情況
不是
promise
對象
如果不是promise
,await
會阻塞后面的代碼,先執行async
外面的同步代碼,同步代碼執行完畢后,在回到async
內部,把promise
的東西,作為await
表達式的結果是promise對象
如果它等到的是一個promise
對象,await
也會暫停async
后面的代碼,先執行async
外面的同步代碼,等著Promise
對象 fulfilled,然后把resolve
的參數作為await
表達式的運算結果。如果一個
Promise
被傳遞給一個await
操作符,await
將等待Promise
正常處理完成并返回其處理結果。
實例3
new Promise( ( resolve, reject ) => {console.log( "promise1" )resolve()} ).then( () => {console.log( 1 )} ).then( () => {console.log( 2 )} ).then( () => {console.log( 3 )} )new Promise( ( resolve, reject ) => {console.log( "promise2" )resolve()} ).then( () => {console.log( 4 )} ).then( () => {console.log( 5 )} ).then( () => {console.log( 6 )} )//1-4-2-5-3-6
先執行同步代碼 promise1, promise2,此時微任務有兩個任務 log(1)和log(4)
執行完log(1)和log(4)此時任務中有log(2)和log(5)兩個微任務
執行log(2)和log(5)此時任務中有log(3)和log(6)兩個微任務
連續的幾個then()回調,并不是連續的創建了一系列的微任務并推入微任務隊列,因為then()的返回值必然是一個Promise,而后續的then()是上一步then()返回的Promise的回調
實例4
setTimeout(() => console.log('setTimeout1'), 0); //1宏任務
setTimeout(() => { //2宏任務console.log('setTimeout2');Promise.resolve().then(() => {console.log('promise3');Promise.resolve().then(() => {console.log('promise4');})console.log(5)})setTimeout(() => console.log('setTimeout4'), 0); //4宏任務
}, 0);
setTimeout(() => console.log('setTimeout3'), 0); //3宏任務
Promise.resolve().then(() => {//1微任務console.log('promise1');
})//promise1
//setTimeout1
//setTimeout2
//promise3
//5
//promise4
//setTimeout3
//setTimeout4