一、運行機制
JavaScript是單線程運行機制宫屠。
為什么JavaScript是單線程列疗?
單線程就是說,js在同一時間只做一件事浪蹂。這要從JavaScript誕生說起抵栈,它最初是設(shè)計用來驗證表單的網(wǎng)頁腳本,并且在這么多年的發(fā)展中也一直是作為與用戶和DOM交互的介質(zhì)語言存在的坤次,所以對用戶來說古劲,用戶在一個網(wǎng)頁上同時只會做一個操作。如果js是并發(fā)的缰猴,那么如果同時操作一個DOM产艾,是會出現(xiàn)問題的。
二滑绒、工作者線程
(《js高程》第27章)
為了利用CPU多核計算能力闷堡,也為了將確實可以后臺等待的事件放在后臺執(zhí)行,js提出了工作者線程的解決方案疑故。
工作者線程是獨立于主執(zhí)行線程環(huán)境的一個子環(huán)境杠览,它除了無法操作DOM等必須單線程操作的API,其他與主執(zhí)行環(huán)境基本一致纵势。
主線程只有一個踱阿,但是工作者線程可以有多個,并且每個工作線程與主線程都是相互獨立的钦铁,每個線程可以并行執(zhí)行腳本软舌。
注意:創(chuàng)建一個新的工作者線程的開銷比較大,所以不建議大量使用牛曹,通常一個工作者線程應(yīng)該是長期運行的佛点。
工作者線程主要分為:專用工作者線程、共享工作者線程和服務(wù)工作者線程躏仇。
三恋脚、執(zhí)行時機
基本名詞概念:
- 同步任務(wù):在主線程上排隊執(zhí)行的任務(wù)腺办,按從上到下的順序執(zhí)行焰手。
- 異步任務(wù):在任務(wù)隊列中等待執(zhí)行的任務(wù)糟描,通常是一個回調(diào)函數(shù)。當(dāng)任務(wù)隊列通知主線程某個異步任務(wù)可以執(zhí)行了书妻,這個任務(wù)就會進入主線程中執(zhí)行船响,任務(wù)隊列是FIFO(first in first out)順序執(zhí)行的。當(dāng)然躲履,定時器(setTimeout见间、setInterval)是一個例外。所以其實同步任務(wù)也可以看做直接執(zhí)行的異步任務(wù)工猜。
- 宏任務(wù)和微任務(wù):異步任務(wù)又分為宏任務(wù)和微任務(wù)米诉,瀏覽器在執(zhí)行時,會先執(zhí)行宏任務(wù)篷帅,再執(zhí)行此宏任務(wù)產(chǎn)生的微任務(wù)史侣,再執(zhí)行下一個宏任務(wù),如此次宏任務(wù)沒有產(chǎn)生微任務(wù)魏身,則會直接執(zhí)行下一個宏任務(wù)惊橱。
常見宏任務(wù):
主線程代碼塊
setTimeout
setInterval
setImmediate ()-Node
requestAnimationFrame ()-瀏覽器
常見微任務(wù)
process.nextTick ()-Node
Promise.then()
catch
finally
Object.observe
MutationObserver
在node環(huán)境下,process.nextTick的優(yōu)先級高于Promise箭昵,可以簡單理解為在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊列中的nextTickQueue部分税朴,然后才會執(zhí)行微任務(wù)中的Promise部分
事件循環(huán)(Event Loop):主線程先執(zhí)行完同步任務(wù),然后從任務(wù)隊列中讀取任務(wù)并執(zhí)行家制,一般來說正林,主線程執(zhí)行異步任務(wù)就是執(zhí)行回調(diào)函數(shù)。執(zhí)行完成之后又從任務(wù)隊列中讀取下一個任務(wù)颤殴,這一個重復(fù)讀取的過程叫做事件循環(huán)觅廓。
NodeJS的Event Loop
Node會先執(zhí)行所有類型為 timers 的 MacroTask,然后執(zhí)行所有的 MicroTask(NextTick例外)
進入 poll 階段诅病,執(zhí)行幾乎所有 MacroTask哪亿,然后執(zhí)行所有的 MicroTask
再執(zhí)行所有類型為 check 的 MacroTask,然后執(zhí)行所有的 MicroTask
再執(zhí)行所有類型為 close callbacks 的 MacroTask贤笆,然后執(zhí)行所有的 MicroTask
至此蝇棉,完成一個 Tick,回到 timers 階段
- 定時觸發(fā)器(setTimeout和setInterval)是在單獨的定時觸發(fā)器線程中執(zhí)行計時的芥永,當(dāng)達到觸發(fā)條件之后篡殷,會將回調(diào)函數(shù)添加到異步隊列中等待執(zhí)行
- http網(wǎng)絡(luò)請求是在XMLHttpRequest鏈接后新開一個線程進行請求,在請求狀態(tài)變化之后埋涧,相應(yīng)得把回調(diào)放進異步線程等待執(zhí)行
其他
setTimeout(fn, 0)
setTimeout(fn,0)的含義是板辽,指定某個任務(wù)在主線程最早可得的空閑時間執(zhí)行奇瘦,也就是說,盡可能早得執(zhí)行劲弦。它在"任務(wù)隊列"的尾部添加一個事件耳标,因此要等到同步任務(wù)和"任務(wù)隊列"現(xiàn)有的事件都處理完,才會得到執(zhí)行邑跪。
setTimeout()只是將事件插入了"任務(wù)隊列"次坡,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)画畅。要是當(dāng)前代碼耗時很長砸琅,有可能要等很久,所以并沒有辦法保證轴踱,回調(diào)函數(shù)一定會在setTimeout()指定的時間執(zhí)行症脂,換句話說,setTimeout是主程序執(zhí)行完畢之后才開始計時
eg.
console.log(Date.parse(new Date()))
setTimeout(()=>{
console.log(Date.parse(new Date()))
console.log('setTimeout')
}, 0)
for (let i = 0; i<10000; i++){
console.count('for')
}
console.log(Date.parse(new Date()))
console.log('endFor')
setImmediate()
與setTimeout(fn, 0)作用相似淫僻,但是執(zhí)行會在setTimeout(fn, 0)之后诱篷,下面兩個例子輸出一模一樣
setTimeout(()=>{
console.log('setTimeout')
}, 0)
setImmediate(()=>{
console.log('setImmediate')
})
console.log('hello world')
setImmediate(()=>{
console.log('setImmediate')
})
setTimeout(()=>{
console.log('setTimeout')
}, 0)
console.log('hello world')
輸出
hello world
setTimeout
setImmediate
經(jīng)典題目
請問以下代碼輸出是什么?
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
setImmediate(() => console.log('setImmediate'));
async1();
process.nextTick(() => console.log('nextTick'));
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
參考文章
阮一峰 JavaScript 運行機制詳解:再談Event Loop
帶你徹底弄懂Event Loop
硬核JS」一次搞懂JS運行機制
《JavaScript高級程序設(shè)計(第四版)》