本文為個人見解,如果發現文章有錯誤的地方,歡迎大家指正,感謝感謝~~
轉載請標明出處
本文針對於Chrome瀏覽器環境下的事件迴圈機制,node環境下還沒有進行試驗,以後再試驗下~~
前言
眾所周知,JavaScript的一大特點就是單執行緒,也就是會按順序執行程式碼,同一時間只能做一件事。
為什麼JavaScript會被設計成單執行緒?
JavaScript的誕生,一開始是為了解決瀏覽器使用者互動的問題,以及用來操作DOM,基於這個原因,JavaScript被設計成單執行緒,否則會帶來複雜的同步問題。
為什麼JavaScript需要非同步?
單執行緒意味著所有任務都要排隊進行,如果存在一個任務執行時間過長,後面的任務都會被阻塞,對於使用者而言就意味著“卡死”。
單執行緒的JavaScript是怎麼執行非同步程式碼的呢?
這就涉及到JavaScript的事件迴圈機制(event loop)了。
事件迴圈機制(event loop)
這裡先推薦去看看Philip Roberts的演講《Help, I’m stuck in an event-loop》,雖然內容沒有涉及到任務佇列的細分,但是對函式呼叫棧(call stack)的分析還是挺不錯的
列舉幾個概念:執行上下文, 函式呼叫棧(call stack), 任務佇列(task queue)
- 執行上下文(以後有空應該會再寫一篇文章分析一下哈哈):
- 全域性環境:JavaScript程式碼執行起來會首先進入該環境
- 函式環境:當函式被呼叫執行時,會進入當前函式中執行程式碼
- eval(不建議使用,可忽略)
- 函式呼叫棧(call stack)是決定了js程式碼的執行機制,遇到函式時,會生成一個新的函式上下文,並且入棧,執行完畢後出棧
- JavaScript中的任務分為macro-task(巨集任務)與micro-task(微任務)
- macro-task包括:script(一段程式碼),setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering
- micro-task包括:process.nextTick, Promise, Object.observe, MutationObserver
-
當JavaScript程式碼開始執行時,首先將全域性環境壓入函式呼叫棧(棧底永遠都是全域性上下文,除非執行緒結束,在瀏覽器上表現為視窗關閉),之後,每遇到一個函式,建立一個新的函式上下文,並且入棧。
-
執行過程中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,比如setTimeout交給timer模組,ajax請求交給network模組,DOM操作交給DOM對應模組處理,處理完成後,會將對應的回撥函式放入對應的佇列中(macro-task佇列以及micro-task佇列)
-
每當函式呼叫棧中的上下文都執行完畢時(全域性環境仍然存在),主程式會去查詢micro-task佇列,如果micro-task佇列為空,會取macro-task佇列第一個task放入呼叫棧執行,否則,取micro-task佇列的第一個task放入呼叫棧執行,如果在處理task期間,如果有新新增的microtasks或者macro-task,也會被新增到相應佇列的末尾
-
一直迴圈第3步,直至所有任務執行完畢,這就是事件迴圈
按照我的思路大概畫了個流程圖
來實踐一下,想象以下程式碼片段的控制檯輸出
console.log('start')
setTimeout(function setTimeout1() {
console.log('setTimeout1')
setTimeout(function setTimeout3() {
console.log('setTimeout3')
new Promise(function promise4(resolve, reject) {
console.log('promise4')
resolve('then')
}).then(function then4() {
console.log('promise4 then')
})
}, 0)
new Promise(function promise3(resolve, reject) {
console.log('promise3')
setTimeout(function setTimeout4() {
resolve('then')
}, 0)
console.log('after resolve')
}).then(function then3() {
console.log('promise3 then')
})
}, 0)
new Promise(function promise1(resolve, reject) {
console.log('promise1')
resolve('then')
}).then(function then1() {
console.log('promise1 then')
new Promise(function promise2(resolve, reject) {
console.log('promise2')
resolve('then')
}).then(function then2() {
console.log('promise2 then')
})
})
setTimeout(function setTimeout2() {
console.log('setTimeout2')
}, 0)
console.log('end')
/*
控制檯輸出
start
promise1
end
promise1 then
promise2
promise2 then
setTimeout1
promise3
after resolve
setTimeout2
setTimeout3
promise3 then
*/
複製程式碼
細化步驟還挺多的,所以做了個gif~~
第一步
全域性上下文global
進棧
第二步
console.log('start')
複製程式碼
遇到console.log
,函式進棧,呼叫web api的console介面,執行完成後出棧
第三步
setTimeout(function setTimeout1() {
//....
}, 0)
複製程式碼
遇到setTimeout
,交給timer
模組執行,setTimeout
出棧,timer
執行完該定時器後(0秒後),將回撥函式setTimeout1
放入macro-task
隊尾。劃重點!!這是一個很容易產生誤解的地方,很多同學下意識都覺得定時器就是到了設定時間後立即執行,其實是到了時間後,將回撥函式放入macro-task
佇列,等待執行
第四步
new Promise(function promise1(resolve, reject) {
console.log('promise1')
resolve('then')
}).then(function then1() {
//...
})
複製程式碼
遇到promise
,建構函式裡的promise1
會立刻進棧並且執行,執行中遇到了resolve
函式,進棧,將回撥函式then1
放入micro-task
佇列,此時promise1
和resolve
都已執行完畢,出棧
第五步
setTimeout(function setTimeout2() {
//...
}, 0)
複製程式碼
遇到setTimeout
,交給timer
模組執行,setTimeout
出棧,timer
執行完該定時器後(0秒後),將回撥函式setTimeout2
放入macro-task
隊尾。
第六步
console.log('end')
複製程式碼
第七步
到了很關鍵的一步,這個時候call stack
已經執行完了(只剩下global),主程式會去查詢micro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then1
)進棧執行
function then1() {
console.log('promise1 then')
new Promise(function promise2(resolve, reject) {
console.log('promise2')
resolve('then')
}).then(function then2() {
//...
})
}
複製程式碼
在執行過程中,又遇到了promise
,先執行建構函式裡的promise2
,執行中遇到了resolve
函式,進棧,將回撥函式then2
放入micro-task
佇列,此時then1
、promise2
和resolve
都已執行完畢,出棧
第八步
call stack
執行完畢,查詢micro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then2
)進棧執行
function then2() {
console.log('promise2 then')
}
複製程式碼
第九步
call stack
執行完畢,查詢micro-task
佇列,發現為空,查詢macro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout1
)進棧執行
function setTimeout1() {
console.log('setTimeout1')
setTimeout(function setTimeout3() {
//...
}, 0)
new Promise(function promise3(resolve, reject) {
console.log('promise3')
setTimeout(function setTimeout4() {
//...
}, 0)
console.log('after resolve')
}).then(function then3() {
//...
})
}
複製程式碼
執行中遇到setTimeout
,交給timer
模組執行,setTimeout
出棧,timer
執行完該定時器後(0秒後),將回撥函式setTimeout3
放入macro-task
隊尾。
繼續執行,遇到了promise
,先執行建構函式裡的promise3
,又遇到了setTimeout
,交給timer
模組執行,setTimeout
出棧,timer
執行完該定時器後(0秒後),將回撥函式setTimeout4
放入macro-task
隊尾,此時setTimeout1
和promise3
都已執行完畢,出棧
第十步
call stack
執行完畢,查詢micro-task
佇列,發現為空,查詢macro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout2
)進棧執行
function setTimeout2() {
console.log('setTimeout2')
}
複製程式碼
第十步
call stack
執行完畢,查詢micro-task
佇列,發現為空,查詢macro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout3
)進棧執行
function setTimeout3() {
console.log('setTimeout3')
new Promise(function promise4(resolve, reject) {
console.log('promise4')
resolve('then')
}).then(function then4() {
console.log('promise4 then')
})
}
複製程式碼
執行中遇到了promise
,先執行建構函式裡的promise4
,遇到了resolve
函式,進棧,將回撥函式then2
放入micro-task
佇列,此時setTimeout3
和promise4
都已執行完畢,出棧
第十一步
call stack
執行完畢,查詢micro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then4
)進棧執行
function then4() {
console.log('promise4 then')
}
複製程式碼
第十二步
call stack
執行完畢,查詢micro-task
佇列,發現為空,查詢macro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是setTimeout4
)進棧執行
function setTimeout4() {
resolve('then')
}
複製程式碼
執行遇到resolve
,將promise
的回撥函式then3
放入micro-task
佇列,此時setTimeout4
和resolve
已執行完畢,出棧
第十三步
call stack
執行完畢,查詢micro-task
佇列,發現裡面有等待執行的函式,取隊首的函式(也就是then3
)進棧執行,執行完畢後出棧,至此全部程式碼執行完畢
呼~終於寫完了
總結
- 主程式開始執行程式碼時,先將全域性環境入棧,以後每遇到一個函式,建立一個新的上下文,進棧並且執行,遇到主程式執行不了的函式,交給web api執行,同時出棧
- 執行過程中,遇到了macro-task或者micro-task,都會將其交給對應的web api去處理,比如setTimeout交給timer模組,ajax請求交給network模組,DOM操作交給DOM對應模組處理,處理完成後,會將對應的回撥函式放入對應的佇列中(macro-task佇列以及micro-task佇列)
- 每當函式呼叫棧中的上下文都執行完畢時(全域性環境仍然存在),主程式會去查詢micro-task佇列,如果micro-task佇列為空,會取macro-task佇列第一個task放入呼叫棧執行,否則,取micro-task佇列的第一個task放入呼叫棧執行,如果在處理task期間,如果有新新增的microtasks或者macro-task,也會被新增到相應佇列的末尾
- 以上內容只針對於Chrome瀏覽器環境,node環境還沒有具體測試,好像是不太一樣的
關於
前端萌新一個~~打算經常寫寫文章總結一下知識點,歡迎關注,一起加油啦