JavaScript事件迴圈詳解
本文已參與「新人創作禮」活動,一起開啟掘金創作之路。
做為一個前端開發,要想深入學習JavaScript進階知識,就不得不瞭解JavaScript的事件迴圈。JavaScript的事件迴圈抽象,不易理解,誰都可以說出單執行緒,巨集任務,微任務,但大部分人只是停留在理論上,在實踐中卻不是太清晰,給大家推薦一個視覺化EventLoop演示網址:JS Visualizer 9000 (jsv9000.app),可以動態演示每個執行步驟,方便大家理解。
1、JS的執行機制
javascript是一門單執行緒語言,預設情況下,既然js是單執行緒,那就像只有一個視窗的銀行,客戶需要排隊一個一個辦理業務,同理js任務也要一個一個順序執行。如果按單執行緒同步的方式執行,一旦HTTP請示向伺服器傳送,就會出現等待資料返回之前網頁假死的效果,這就導致頁面渲染和事件的執行,在這個過程中無法進行。顯然在實際開發中並沒有出現這種情況。
關於同步和非同步
基於以上的描述,我們知道在JavaScript的世界中,應該有一種方案,來處理單執行緒造成的問題。這就是同步和非同步執行模式的出現。
同步(阻塞):
同步的意思是JavaScript會嚴格按照單執行緒(從上到下,從左到右的方式)執行程式碼邏輯。
js
let a = 1
let b = 2
let c = a + b
function sleep(ms) {
let start = Date.now()
while (Date.now() - start < ms) {}
}
console.log('a=', a)
console.log('b=', b)
sleep(2000)
console.log('c=', c)
非同步(非阻塞)
js
let a = 1
let b = 2
let c = a + b
console.log('a=', a)
console.log('b=', b)
/*非同步程式碼*/
setTimeout(() => {
console.log('2秒後輸出setTimeout')
}, 2000)
console.log('c=', c)
非阻塞式執行的程式碼,程式執行到該程式碼片段時,執行引擎人將程式儲存到一個暫存區,等待所有同步程式碼全部執行完畢後,非阻塞式的程式碼會按照特定的執行順序,分步執行。這就是單執行緒非同步的特點。
總結
JavaScript的執行順序就是完全單執行緒的非同步模型:同步在前,非同步在後。所有的非同步任務都要等待當前的同步任務執行完畢之後才能執行。
2、JS的執行緒組成
雖然瀏覽器是單執行緒執行JavaScript程式碼的,但是瀏覽器實際是以多個執行緒協助操作來實現單執行緒非同步模型的,具體執行緒組成如下:
- GUI渲染執行緒
- JavaScript引擎執行緒
- 事件觸發執行緒(按鈕、事件)
- 定時器觸發執行緒
- http請示執行緒
- 其他執行緒
在JavaScript程式碼執行的過程中實際執行程式時,同時只存在一個活動執行緒,這裡實現同步非同步就是靠多執行緒切換的形式來實現的。
所以通常我們將上面的細分執行緒歸納為下列兩條執行緒:
- 【主執行緒】:這個執行緒用來執行頁面的渲染,JavaScript程式碼的執行,事件的觸發等等
- 【工作執行緒】:這個執行緒是在幕後工作的,用來處理非同步任務的執行來實現非阻塞的執行模式
3、JavaScript的執行模型
```js
function logA() {
console.log('A')
setTimeout(()=>{
console.log('logA-setTimeout')
})
}
function logB() { console.log('B') }
function logC() { console.log('C') }
function logD() { console.log('D') }
logA(); // 程式碼1 setTimeout(logB, 1000); // 程式碼2 Promise.resolve().then(logC); // 程式碼3 logD(); // 程式碼4 ``` 上述程式碼,
第一輪執行
1、首先執行程式碼1,同步程式碼A直接執行,setTimeout放到Task Queue非同步工作佇列中
2、然後執行程式碼2,setTimeout非同步任務,把logB放入到Task Queue非同步工作佇列中
3、程式碼3,Promise微任務,將logC放到Microtask Queue微任務佇列
4、程式碼4,同步程式碼,直接放入執行棧中執行,並出棧
當同步程式碼執行完,會清空微任務佇列,然後才會執行巨集任務佇列,見下圖,繼續執行
執行棧
執行棧是一個棧的資料結構,當我們執行單層函式時,執行棧執行的函式進棧後,會出棧銷燬然後下一個進棧下一個出棧,當有函式巢狀呼叫的時候棧中就會堆積棧幀
```js
function sixth() { }
function fifth() { sixth() }
function fourth() { fifth() }
function third() { fourth() }
function second() { third() }
function first() { second() }
```
關於遞迴
遞迴函式是專案開發時經常涉及到場景。在未知嘗試的樹形結構,或其他合適的場景中使用遞迴。遞迴的風險問題:如果發解了執行棧的執行邏輯後,遞迴函式就可以看成是在一個函式中巢狀n層執行,那麼在執行過程中會觸發大量的棧幀堆積,如果處理的資料過大,會導致執行棧的高度不夠放置新的棧幀,而造成棧溢位的錯誤。
如何跨越遞迴限制
js
var i = 0
function task() {
let index = i++
console.log(`遞迴了${index}次`)
// task()
setTimeout(tack,0)// 讓遞迴正常出棧
console.log(`遞迴了${index}次,完成`)
}
task()
如何能通過技術手段跨越遞迴的限制。可以將程式碼做如下更改,這樣就不會出現遞迴問題了。
有了非同步任務之後遞迴就不會疊加棧幀了,因為放入工作執行緒之後該函式就結束了,可以出棧銷燬,那麼在執行棧中就永遠只有一個任務在執行,這樣就防止了棧幀的無限疊加,從而解決了無限遞迴的問題,不過非同步遞迴的過程是無法保證速度的,在實際的工作場景中,如果考慮效能問題,儘量避免遞迴迴圈,因為遞迴迴圈就算控制在有限棧幀的疊加,其效能也遠遠不及指標迴圈。
4、巨集任務和微任務
任務佇列的資料結構是佇列結構。所有除同步任務外的程式碼都會在工作執行緒中,按照他到達的時間節點有序的進入任務佇列,而且任務佇列中非同步任務又分為【巨集任務】和【微任務】。
巨集任務
巨集任務是JavaScript中最原始的非同步任務,包括setTimeout、setInterVal、AJAX等,在程式碼執行環境中按照同步程式碼的順序,逐個進入工作執行緒掛起,再按照非同步任務到達的時間節點,逐個進入非同步任務佇列,最終按照佇列中的順序進入函式執行棧進行執行。
微任務
微任務是隨著ECMA標準升級提出的新的非同步任務,微任務在非同步任務佇列的基礎上增加了【微任務】的概念,每個巨集任務執行前,程式會先檢測程式碼中是否有當次事件迴圈未執行的微任務,優先清空本次的微任務後,再執行下一個巨集任務,每一個巨集任務內部可註冊當次任務的微任務佇列,再下一個巨集任務執行前執行,微任務也是按照進入佇列的順序執行的。
js
/*巨集任務 微任務
* 同一作用域下:同步程式碼>微任務>巨集任務
*/
var a = '我是同步程式碼'
setTimeout(function () {
// 同一作用域下:同步程式碼>微任務>巨集任務
Promise.resolve().then(function () {
console.log('Macro1-Micro1:我是第一個巨集任務中的微任務')
})
console.log('Macro1:我是巨集任務1')
setTimeout(function () {
console.log('Macro1-Macro1:我是第一個巨集任務中的巨集任務')
})
})
setTimeout(function () {
console.log('Macro2:我是巨集任務2')
})
Promise.resolve().then(function () {
console.log('Micro0:我是微任務0')
})
console.log(a)
Promise
Promise是(同步執行),但Promise 的回撥函式屬於非同步任務,會在同步任務之後執行(比如說 then、 catch 、finally)
```js new Promise(function(res,rej){ console.log('AAA') res('CCC') console.log('BBB') }).then(res=>{ console.log(res) })
console.log('A') // 執行順序:AAA>BBB>A>CCC ```