前言
大家都知道js是單執行緒的指令碼語言,在同一時間,只能做同一件事,為了協調事件、使用者互動、指令碼、UI渲染和網路處理等行為,防止主執行緒阻塞,Event Loop方案應運而生...
個人部落格瞭解一下:obkoro1.com
為什麼js是單執行緒?
js作為主要執行在瀏覽器的指令碼語言,js主要用途之一是操作DOM。
在js高程中舉過一個栗子,如果js同時有兩個執行緒,同時對同一個dom進行操作,這時瀏覽器應該聽哪個執行緒的,如何判斷優先順序?
為了避免這種問題,js必須是一門單執行緒語言,並且在未來這個特點也不會改變。
執行棧與任務佇列
因為js是單執行緒語言,當遇到非同步任務(如ajax操作等)時,不可能一直等待非同步完成,再繼續往下執行,在這期間瀏覽器是空閒狀態,顯而易見這會導致巨大的資源浪費。
執行棧
當執行某個函式、使用者點選一次滑鼠,Ajax完成,一個圖片載入完成等事件發生時,只要指定過回撥函式,這些事件發生時就會進入任務佇列中,等待主執行緒讀取,遵循先進先出原則。
執行任務佇列中的某個任務,這個被執行的任務就稱為執行棧。
主執行緒
要明確的一點是,主執行緒跟執行棧是不同概念,主執行緒規定現在執行執行棧中的哪個事件。
主執行緒迴圈:即主執行緒會不停的從執行棧中讀取事件,會執行完所有棧中的同步程式碼。
當遇到一個非同步事件後,並不會一直等待非同步事件返回結果,而是會將這個事件掛在與執行棧不同的佇列中,我們稱之為任務佇列(Task Queue)。
當主執行緒將執行棧中所有的程式碼執行完之後,主執行緒將會去檢視任務佇列是否有任務。如果有,那麼主執行緒會依次執行那些任務佇列中的回撥函式。
不太理解的話,可以執行一下下面的程式碼,或者點選一下這個demo
結果是當a、b、c函式都執行完成之後,三個setTimeout才會依次執行。
let a = () => {
setTimeout(() => {
console.log('任務佇列函式1')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('a的for迴圈')
}
console.log('a事件執行完')
}
let b = () => {
setTimeout(() => {
console.log('任務佇列函式2')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('b的for迴圈')
}
console.log('b事件執行完')
}
let c = () => {
setTimeout(() => {
console.log('任務佇列函式3')
}, 0)
for (let i = 0; i < 5000; i++) {
console.log('c的for迴圈')
}
console.log('c事件執行完')
}
a();
b();
c();
// 當a、b、c函式都執行完成之後,三個setTimeout才會依次執行
複製程式碼
js 非同步執行的執行機制。
- 所有任務都在主執行緒上執行,形成一個執行棧。
- 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
- 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列"。那些對應的非同步任務,結束等待狀態,進入執行棧並開始執行。
- 主執行緒不斷重複上面的第三步。
巨集任務與微任務:
非同步任務分為 巨集任務(macrotask) 與 微任務 (microtask),不同的API註冊的任務會依次進入自身對應的佇列中,然後等待 Event Loop 將它們依次壓入執行棧中執行。
巨集任務(macrotask)::
script(整體程式碼)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 環境)
微任務(microtask):
Promise、 MutaionObserver、process.nextTick(Node.js環境)
Event Loop(事件迴圈):
Event Loop(事件迴圈)中,每一次迴圈稱為 tick, 每一次tick的任務如下:
- 執行棧選擇最先進入佇列的巨集任務(通常是
script
整體程式碼),如果有則執行 - 檢查是否存在 Microtask,如果存在則不停的執行,直至清空 microtask 佇列
- 更新render(每一次事件迴圈,瀏覽器都可能會去更新渲染)
- 重複以上步驟
巨集任務 > 所有微任務 > 巨集任務,如下圖所示:
從上圖我們可以看出:
- 將所有任務看成兩個佇列:執行佇列與事件佇列。
- 執行佇列是同步的,事件佇列是非同步的,巨集任務放入事件列表,微任務放入執行佇列之後,事件佇列之前。
- 當執行完同步程式碼之後,就會執行位於執行列表之後的微任務,然後再執行事件列表中的巨集任務
上面提到的demo結果可以這麼理解:先執行script
巨集任務,執行完了之後,再執行其他兩個定時器巨集任務。
面試題實踐
下面這個題,很多人都應該看過/遇到過,重新來看會不會覺得清晰很多:
// 執行順序問題,考察頻率挺高的,先自己想答案**
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
console.log(4);
複製程式碼
根據本文的解析,我們可以得到:
-
先執行
script
同步程式碼先執行new Promise中的console.log(2),then後面的不執行屬於微任務 然後執行console.log(4) 複製程式碼
-
執行完
script
巨集任務後,執行微任務,console.log(3),沒有其他微任務了。 -
執行另一個巨集任務,定時器,console.log(1)。
根據本文的內容,可以很輕鬆,且有理有據的猜出寫出正確答案:2,4,3,1.
結語
類似上文的面試題還有很多,實則都大同小異,只要掌握了事件迴圈的機制,這些問題都會變得很簡單。
文章如有不正確的地方歡迎各位路過的大佬鞭策!希望大家看完可以有所收穫,喜歡的話,趕緊點波訂閱關注/喜歡。
看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。
個人blog and 掘金個人主頁,如需轉載,請放上原文連結並署名。碼字不易,感謝支援!
如果喜歡本文的話,歡迎關注我的訂閱號,漫漫技術路,期待未來共同學習成長。
以上2018.6.16