javascript執行機制是基于事件循環的并發式的,事件循環負責處理代碼,收集和處理事件以及執行隊列中的子任務
棧(stack)
js運行時形成一個執行棧,
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
當調用 bar 時,第一個幀被創建并壓入棧中,幀中包含了 bar 的參數和局部變量。 當 bar 調用 foo 時,第二個幀被創建并被壓入棧中,放在第一個幀之上,幀中包含 foo 的參數和局部變量。當 foo 執行完畢然后返回時,第二個幀就被彈出棧(剩下 bar 函數的調用幀 )。當 bar 也執行完畢然后返回時,第一個幀也被彈出,棧就被清空了。
堆(heap)
對象被分配在堆中,堆是一個用來表示一大塊(通常是非結構化的)內存區域的計算機術語。
隊列(queue)
一個 JavaScript 運行時包含了一個待處理消息的消息隊列。每一個消息都關聯著一個用以處理這個消息的回調函數。
setTimeout(function(){console.log('時間一到,請執行改回調函數')},1000)
在 事件循環 期間的某個時刻,運行時會從最先進入隊列的消息開始處理隊列中的消息。被處理的消息會被移出隊列,并作為輸入參數來調用與之關聯的函數。正如前面所提到的,調用一個函數總是會為其創造一個新的棧幀。
事件循環(event loop)
queue.waitForMessage() 會同步地等待消息到達(如果當前沒有任何消息等待被處理)
while (queue.waitForMessage()) {
queue.processNextMessage();
}
特點
- 單線程
每一個消息完整地執行后,其它消息才會被執行
缺點:當一個消息需要太長時間才能處理完畢時,Web應用程序就無法處理與用戶的交互,例如點擊或滾動
2.永不阻塞
JavaScript的事件循環模型與許多其他語言不同的一個非常有趣的特性是,它永不阻塞。 處理 I/O 通常通過事件和回調來執行,所以當一個應用正等待一個 IndexedDB 查詢返回或者一個 XHR 請求返回時,它仍然可以處理其它事情,比如用戶輸入。
總結一下事件循環的機制:
(1) 所有任務在執行棧上執行,
(2) 綁定事件和異步事件(消息)放置于任務隊列
(3) 執行棧執行完畢,一直詢問任務隊列是否有消息需要執行,如果有就將關聯的回調函數放置于執行棧,準備執行
宏任務(macrotask)和微任務(mircrotask)
異步任務細分為宏任務和微任務,當一個宏任務執行完,會在渲染前,將執行期間所產生的所有微任務都執行完
宏任務 -> 微任務 -> GUI渲染 -> 宏任務 -> ...
宏任務:
- 主代碼塊
- setTimeout
- setTimeInterval
- setImmediate()-node
- requestAnimationFrame -瀏覽器
- postMessage
- I/O
- UI交互事件
微任務: - process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
注意點
- 瀏覽器會先執行一個宏任務,緊接著執行當前執行棧產生的微任務,再進行渲染,然后再執行下一個宏任務
- 微任務和宏任務不在一個任務隊列
總結
*執行主線程,遇到異步置入任務隊列,并在任務隊列根據宏任務和微任務進行區分
*執行棧完成,查看任務隊列,首先查看宏任務隊列有沒有要執行的任務,沒有就過,有就執行
- 每個宏任務執行完都要查看微任務隊列,有沒有要執行的任務,沒有就過,有就執行,直到微任務隊列為空
測試:
function test() {
console.log(1)
setTimeout(function () { // timer1
console.log(2)
}, 1000)
}
test();
setTimeout(function () { // timer2
console.log(3)
})
new Promise(function (resolve) {
console.log(4)
setTimeout(function () { // timer3
console.log(5)
}, 100)
resolve()
}).then(function () {
setTimeout(function () { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
結合我們上述的JS運行機制再來看這道題就簡單明了的多了
JS是順序從上而下執行
執行到test(),test方法為同步,直接執行,console.log(1)打印1
test方法中setTimeout為異步宏任務,回調我們把它記做timer1放入宏任務隊列
接著執行,test方法下面有一個setTimeout為異步宏任務,回調我們把它記做timer2放入宏任務隊列
接著執行promise,new Promise是同步任務,直接執行,打印4
new Promise里面的setTimeout是異步宏任務,回調我們記做timer3放到宏任務隊列
Promise.then是微任務,放到微任務隊列
console.log(8)是同步任務,直接執行,打印8
主線程任務執行完畢,檢查微任務隊列中有Promise.then
開始執行微任務,發現有setTimeout是異步宏任務,記做timer4放到宏任務隊列
微任務隊列中的console.log(7)是同步任務,直接執行,打印7
微任務執行完畢,第一次循環結束
檢查宏任務隊列,里面有timer1、timer2、timer3、timer4,四個定時器宏任務,按照定時器延遲時間得到可以執行的順序,即Event Queue:timer2、timer4、timer3、timer1,依次拿出放入執行棧末尾執行 (插播一條:瀏覽器 event loop 的 Macrotask queue,就是宏任務隊列在每次循環中只會讀取一個任務)
執行timer2,console.log(3)為同步任務,直接執行,打印3
檢查沒有微任務,第二次Event Loop結束
執行timer4,console.log(6)為同步任務,直接執行,打印6
檢查沒有微任務,第三次Event Loop結束
執行timer3,console.log(5)同步任務,直接執行,打印5
檢查沒有微任務,第四次Event Loop結束
執行timer1,console.log(2)同步任務,直接執行,打印2
檢查沒有微任務,也沒有宏任務,第五次Event Loop結束
結果:1,4,8,7,3,6,5,2
console.log('start')
setTimeout(function(){
console.log('宏任務1號')
})
Promise.resolve().then(function(){
console.log('微任務0號')
})
console.log('執行棧執行中')
setTimeout(function(){
console.log('宏任務2號')
Promise.resolve().then(function(){
console.log('微任務1號')
})
},500)
setTimeout(function(){
console.log('宏任務3號')
setTimeout(function(){
console.log('宏任務4號')
Promise.resolve().then(function(){
console.log('微任務2號')
})
},500)
Promise.resolve().then(function(){
console.log('微任務3號')
})
},600)
console.log('end')