js 異步、棧、事件循環、任務隊列
在開發中經常遇到js的異步問題,為了方便理解,記錄下來,隨時回顧。
- 以下的所有代碼都是在瀏覽器環境下運行
在瀏覽器中js的運行是依賴瀏覽器js引擎來解析的,並且是在一定的runtime(運行時)的環境被調用,被執行。由於js引擎是單線程的,所以在執行dom渲染,script引入的時候這些操作是同步的,js引擎會通過 Event Loop 的機制,按順序把任務放入棧中執行
而在代碼中產生的異步代碼則是由 runtime 提供的,擁有和Js引擎互不干擾的線程
棧
棧是一個后進先出的一種數據結構,執行起來效率比較高,往往堆里存放着一些對象。而棧中則存放着一些基礎類型變量以及對象的指針,在函數調用的時候,會產生函數的執行棧,也叫執行上下文,這個執行環境中存在着這個函數的私有作用域,上層作用域的指向,函數的參數,這個作用域中定義的變量以及這個作用域的this對象。 而當一系列函數被依次調用的時候,因為js是單線程的,同一時間只能執行一個函數,於是這些函數被排隊在一個單獨的地方。這個地方被稱為執行棧。
當一個腳本第一次執行的時候,js引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧中,然后從頭開始執行。如果當前執行的是一個方法,那么js會向執行棧中添加這個方法的執行環境,然后進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼 執行完畢並返回結果后,js會退出這個執行環境並把這個執行環境銷毀,回到上一個方法的執行環境。。這個過程反復進行,直到執行棧中的代碼全部執行完畢。
總的來說
- 棧存放着一些基礎類型變量以及對象的指針
- 當代碼執行的時候,同步代碼按照執行順序開始執行
- 當代碼執行的時候,碰到函數,引擎會在棧里產生這個函數執行棧,也叫執行上下文。
- 當代碼執行到函數的時候,會進入這個執行環境繼續執行其中的代碼,反復進行,全部執行完
任務隊列
Js 中,有兩類任務隊列:宏任務隊列(macro tasks)和微任務隊列(micro tasks)。宏任務隊列可以有多個,微任務隊列只有一個
- 宏任務:script(全局任務), setTimeout, setInterval, setImmediate, I/O, UI rendering.
- 微任務:process.nextTick (node.js中進程相關的對象), Promise, Object.observer, MutationObserver。
在瀏覽器的 Event Loop機制中,整個流程可以用張圖來表示一下:
這張圖中可以看到:
- 微任務隊列(micro tasks)只會有一個
- 宏任務隊列(macro tasks)可以有多個
- click ajax 等回調方法都會進入到宏任務隊列(macro tasks)中,當然也包括上面的
而在瀏覽器的Event Loop機制運行時,宏任務隊列(macro tasks)和微任務隊列(micro tasks)的關系,關於這點詳見(關系)
- 宏任務按順序執行,且瀏覽器在每個宏任務之間渲染頁面
- 所有微任務也按順序執行,且在以下場景會立即執行所有微任務
- 每個回調之后且js執行棧中為空。
- 每個宏任務結束后。
而在 NodeJs 的 Event Loop 遵循的是 libuv
這個庫是node作者自己寫的,內部實現了一整套的異步io機制(內部使用c++和js實現),使我們開發異步程序變得簡單,因為這個原因導致了一些js解析和瀏覽器的會不一樣。
NodeJs 的運行是這樣的:
- 初始化 Event Loop
- 執行您的主代碼。這里同樣,遇到異步處理,就會分配給對應的隊列。直到主代碼執行完畢。
- 執行主代碼中出現的所有微任務:先執行完所有nextTick(),然后在執行其它所有微任務。
- 開始 Event Loop
當每個階段執行完畢后,都會執行所有微任務(先 nextTick,后其它),然后再進入下一個階段。
最后我們來段代碼徹底解析下兩類任務隊列在運行時的邏輯
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
結合上面講的邏輯 可以分析一波得出最后答案是1,7,6,8,2,4,3,5,9,11,10,12