Event Loop,事件環,執行緒程式。這些概念對初識前端的同學來說可能會一頭霧水。而且執行js程式碼的執行環境除了瀏覽器還有node。因此不同環境處理Event Loop又變得不同,十分容易混淆。如果你有這樣的疑問。下文將給你一個清晰的解釋。
概念梳理
首先我們簡化一下概念,把程式,執行緒,事件環,這些概念梳理一下。清晰了概念後面用到的時候就會有共鳴。
程式和執行緒基本概念
拿出在教科書裡的概念:
1、排程:執行緒作為排程和分配的基本單位,程式作為擁有資源的基本單位;
2、併發性:不僅程式之間可以併發執行,同一個程式的多個執行緒之間也可併發執行;
3、擁有資源:程式是擁有資源的一個獨立單位,執行緒不擁有系統資源,但可以訪問隸屬於程式的資源;
4、系統開銷:在建立或撤消程式時,由於系統都要為之分配和回收資源,導致系統的開銷明顯大於建立或撤消執行緒時的開銷。
程式和執行緒的關係:
- 一個執行緒只能屬於一個程式,而一個程式可以有多個執行緒,但至少有一個執行緒;
- 資源分配給程式,同一程式的所有執行緒共享該程式的所有資源;
- 處理機分給執行緒,即真正在處理機上執行的是執行緒;
- 執行緒在執行過程中,需要協作同步。不同程式的執行緒間要利用訊息通訊的辦法實現同步。執行緒是指程式內的一個執行單元,也是程式內的可排程實體。
第一次看可能並沒什麼共鳴。但是帶著最基本的想法,一個程式可以有多個執行緒,執行緒之間可以相互通訊。這兩點,就足夠你理解後續事件環的知識。
瀏覽器中的程式和執行緒和Event Loop
瀏覽器的程式
- 從開啟瀏覽器開始,開啟瀏覽器,我們首先看到的是,使用者介面,這裡有搜尋框,顯示區,還有收藏夾等等。這些會分配一個程式。
- 我們看到瀏覽器自己會實現一些本地儲存,cookie等,這些操作也需要分配一個程式。
瀏覽器核心的執行緒
接下來看一下瀏覽器引擎(程式)中包含哪些執行緒
- UI渲染執行緒 負責渲染瀏覽器介面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。 當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行
注意
:UI渲染執行緒與JS引擎執行緒是互斥的,當JS引擎執行時GUI執行緒會被掛起(相當於被凍結了),UI更新會被儲存在一個佇列中等到JS引擎空閒時立即被執行。
- js引擎執行緒(JS解析執行緒) 也稱為JS核心,負責處理Javascript指令碼程式。(例如V8引擎) JS引擎執行緒負責解析Javascript指令碼,執行程式碼。 JS引擎一直等待著任務佇列中任務的到來,然後加以處理,一個Tab頁(renderer程式)中無論什麼時候都__只有一個JS執行緒在執行JS程式__
同樣注意
:UI渲染執行緒與JS引擎執行緒是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。
- 事件觸發執行緒 __歸屬於瀏覽器__而不是JS引擎,用來控制事件迴圈(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開執行緒協助) 當JS引擎執行程式碼塊如setTimeOut時(也可來自瀏覽器核心的其他執行緒,如滑鼠點選、AJAX非同步請求等),會將對應任務新增到事件執行緒中 當對應的事件符合觸發條件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理
注意
:由於JS的單執行緒關係,所以這些待處理佇列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時才會去執行)
- 定時觸發器執行緒 傳說中的setInterval與setTimeout所線上程 瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確) 因此通過單獨執行緒來計時並觸發定時(計時完畢後,新增到事件佇列中,等待JS引擎空閒後執行)
注意
:W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。
- 非同步http請求執行緒 在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求 將檢測到狀態變更時,如果設定有回撥函式,非同步執行緒就產生狀態變更事件,將這個回撥再放入事件佇列中。再由JavaScript引擎執行。
js渲染引擎的Event Loop
以上執行緒,每個拿出來都可以詳細的說上一篇。Event Loop涉及到的JS引擎的一些執行機制的分析。我們可以將這些執行緒理解為,
- 一個主程式就是js引擎,其他均為輔助的執行緒。
- 主程式存在一個執行棧,事件觸發執行緒維護一個訊息佇列
- 同步任務在執行棧中執行,非同步任務在滿足條件後加入到訊息佇列中,等待執行。
- 先執行棧中的任務,執行完畢後,檢查佇列是否為空,不為空,將佇列中的任務壓入執行棧中執行。直到棧和佇列均為空。 js渲染引擎的Event Loop如下圖
setTimeout(function(){
console.log(0)
},500)
setTimeout(function(){
console.log(1)
},1000)
setTimeout(function(){
console.log(2)
},2000)
for(;;){
}
複製程式碼
上面這段程式碼用於不會有輸出,同步程式碼死迴圈阻塞了執行棧。雖然定時後回撥加入執行佇列,但是異永遠不會執行。
題目二:
setTimeout(function(){
console.log('setTimeout1');
Promise.resolve().then(()=>{
console.log('then1');
});
},0)
Promise.resolve().then(()=>{
console.log('then2');
Promise.resolve().then(()=>{
console.log('then3');
})
setTimeout(function(){
console.log('setTimeout2');
},0)
})
複製程式碼
答案:then2 then3 setTimeout1 then1 setTimeout2
首先在題目中出現了es6的promise,他的出現讓原來我們理解的__事件環產生了一些不同__。
為什麼呢?因為Promise裡有了一個一個新的概念:microtask
此時JS中分為__兩種任務型別__:macrotask和microtask,在ECMAScript中,microtask稱為jobs,macrotask可稱為task
微任務和巨集任務
首先說明,是以__瀏覽器為處理環境__下的執行邏輯
瀏覽器環境下的微任務和巨集任務有哪些
巨集任務:setTimeout setImmediate MessageChannel
微任務:Promise.then MutationObserver
記住兩點:
- 微任務在巨集任務之前的執行,先執行 執行棧中的內容 執行後 清空微任務
- 每次取一個巨集任務 就去清空微任務,之後再去取巨集任務
然後題目入手分析巨集任務和微任務的執行
- setTimeout1放入巨集任務執行佇列中,微任務then2放入微任務佇列中,棧為空,優先執行微任務,則先執行then2。
- then2之後執行後,接下來存在微任務then3。將then3放入微任務佇列中。
- 接下來setTimeout2加入到巨集任務佇列中。
- 此時執行棧為空,執行then3。
- 微任務全部執行完畢後,執行巨集任務setTimeout1,執行發現微任務then1,放置到微任務佇列中。
- setTimeout1巨集任務執行完,再次清空微任務佇列,執行then1
- 微任務全部執行完畢後,執行巨集任務setTimeout2。程式結束。
node執行環境中的程式和執行緒
Node.js 是一個基於 Chrome V8 引擎的 JavaScript 執行環境。他的目標就是解析js程式碼,讓他能執行起來。
node js 是單執行緒的
和瀏覽器環境下類似,他有一個解析js的主執行緒,其他執行緒作為輔助,但是因為不涉及操作dom,ui執行緒就不存在了。(各個執行緒的概念參考瀏覽器環境下的執行緒)
單執行緒在瀏覽器執行環境中的弊端體現在阻塞頁面執行。
那麼node作為後端服務,單執行緒有什麼利弊?
優點:
- 避免頻繁建立、切換程式的開銷,使執行速度更加迅速。
- 資源佔用小
- 執行緒安全,不用擔心同一變數同時被多個執行緒進行讀寫而造成的程式崩潰。 缺點:
- 不適合大量的計算和壓縮等cpu密集型的操作,會造成阻塞。
node下Event Loop
事件環的整體還是不變的,執行棧,訊息佇列,api。不同的是,node下的訊息佇列有所不同
分析一下node下的訊息佇列- 為微任務,定時器,io,setImmidiate分別分配訊息佇列
- 先檢查定時器佇列,如果有內容,則全部清空
- 從時間佇列切換到io佇列的過程中,檢查微任務,如果有則情況微任務。
- io佇列執行完成,如果有check佇列的內容,則執行。否則繼續檢查定時器佇列。
- 完成閉環
從一個題目入手感受一下node環境和瀏覽器環境下的不同
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise');
});
}, 0)
setTimeout(() => {
console.log('timeout2');
}, 0)
複製程式碼
瀏覽器下的結果:timeout1 promise timeout2
node下的結果:timout1 timeout2 promise
微任務和巨集任務
node環境下的微任務和巨集任務有哪些
巨集任務:setTimeout setImmediate
微任務:Promise.then process.nextTick
題目三可以很好的分析node環境下的任務執行
node環境下執行流程
- 首先遇到兩個巨集任務,均放入到時間佇列裡。
- 執行時間佇列裡第一個巨集任務時timeout1,遇到微任務promise,放到微任務佇列中
- 此時時間佇列還未清空,繼續執行完成所有時間佇列裡的任務。執行timout2
- 在切換io佇列時檢查微任務,有則執行清空微任務。執行promise。
瀏覽器環境下執行流程 - 首先遇到兩個巨集任務,均放入到巨集任務佇列裡。
- 執行時間佇列裡第一個巨集任務時timeout1,遇到微任務promise,放到微任務佇列中
- timout1執行完成檢查微任務,有內容則執行清空,執行promise。
- 清空微任務後再執行巨集任務。執行timeout2
注意
:同樣是微任務,process.nextTick,優於promise.then先執行
Promise.resolve().then(() => {
console.log('then')
})
process.nextTick(() => {
console.log('nextTick')
});
//nextTick then
複製程式碼
注意
:同樣是巨集任務,setTimeout和setImediate執行的先後順序是不確定的,依賴於執行棧執行的速度。
setImmediate(function () {
console.log('setImmediate')
});
setTimeout(function () {
console.log('setTimeout')
}, 0); // ->4
複製程式碼
但是在如下場景下是有固定輸出的
let fs = require('fs');
fs.readFile('./gitignore', function () { // io的下一個事件佇列是check階段
setImmediate(function () {
console.log('setImmediate')
});
setTimeout(function () {
console.log('setTimeout')
}, 0); // ->4
})
複製程式碼
給個提示,讀檔案是io操作,io執行之後首先要check,check之後或沒有check內容再去檢查定時佇列。 那麼結果就留給大家自行分析了。
總結
希望這篇文章能給初識js的你一個清晰的大框,也是梳理我自己的知識。可能我理解的也很粗淺,有錯誤的地方,希望大家幫忙指正。