一. js運行機制
js是單線程,但是存在同步【阻塞】和異步【非阻塞】執(zhí)行模式
- 同步:從上到下、從左到右的?式執(zhí)?代碼邏輯
- 異步:和同步對?,所以異步模式的代碼是不會按照默認順序執(zhí)?的。在解釋時,如果遇到異步模式的代碼,引擎會將當前的任務“掛起”并略過
瀏覽器的線程組成
1. GUI渲染線程
2. JavaScript引擎線程
3. 事件觸發(fā)線程
4. 定時器觸發(fā)線程
5. http請求線程
6. 其他線程
在JavaScript代碼運?的過程中實際執(zhí)?程序時同時只存在?個活動線程,這?實現(xiàn)同步異步就是靠 多線程切換的形式來進?實現(xiàn)的。
所以我們通常分析時,將上?的細分線程歸納為下列兩條線程:
- 【主線程】:這個線程?了執(zhí)???的渲染,JavaScript代碼的運?,事件的觸發(fā)等等
- 【?作線程】:這個線程是在幕后?作的,?來處理異步任務的執(zhí)?來實現(xiàn)?阻塞的運?模式
二. 事件循環(huán)機制
-
宏任務:
宏任務是JavaScript中最原始的異步任務,包括setTimeout、setInterVal、AJAX等,在代碼執(zhí)?環(huán)境中按照同步代碼的順序,逐個進??作線程掛起,再按照異步任務到達的時間節(jié)點,逐個進?異步任務隊列,最終按照隊列中的順序進?函數(shù)執(zhí)?棧進?執(zhí)?。
1 `script` 2 setTimeout 3 setInterval 4 setImmediate(node) 5 I/O 網(wǎng)絡請求完成、文件讀寫完成事件 6 UI rendering 7 requestAnimationFrame(瀏覽器) 8 用戶交互事件(比如鼠標點擊、滾動頁面、放大縮小等) -
微任務:
微任務是隨著ECMA標準升級提出的新的異步任務,微任務在異步任務隊列的基礎(chǔ)上增加了【微任務】的概念,每 ?個宏任務執(zhí)?前,程序會先檢測中是否有當次事件循環(huán)未執(zhí)?的微任務,優(yōu)先清空本次的微任務后,再執(zhí)?下? 個宏任務,每?個宏任務內(nèi)部可注冊當次任務的微任務隊列,再下?個宏任務執(zhí)?前運?,微任務也是按照進?隊列的順序執(zhí)?的。
1. process.nextTick(node) 2. MutationObserver(瀏覽器) 3. promise.then/.catch/.finally【注】:new promise中的function是同步的,promise的.then.catch.finally中的內(nèi)容才是異步的
-
圖解
主線程代碼在運?時,會按照同步和異步代碼將其分成兩個去處:如果是同步代碼執(zhí)?,就會直接將該任務放在?個叫做【函數(shù)執(zhí)??!康目臻g進?執(zhí)?,執(zhí)?棧是典型的【棧結(jié)構(gòu)】(先進后出),程序在運?的時候會將同步代碼按順序?棧。將異步代碼放到【?作線程】中暫時掛起,【?作線程】中保存的是定時任務函數(shù)、JS的交互事件、JS的?絡請求等耗時操作。
當【主線程】將代碼塊篩選完畢后,進?執(zhí)?棧的函數(shù)會按照從外到內(nèi)的順序依次執(zhí)行,運?中涉及到的對象數(shù)據(jù)是在堆內(nèi)存中進?保存和管理的。當執(zhí)?棧內(nèi)的任務全部執(zhí)?完畢 后,執(zhí)?棧就會清空。
執(zhí)?棧清空后,“事件循環(huán)”就會?作,“事件循環(huán)”會檢測本次循環(huán)中是否還有微任務執(zhí)行,若有的話,將所有微任務插入到任務隊列后執(zhí)行清空所有微任務。
微任務清空后,?作線程會把到期的定時任務、返回數(shù)據(jù)的http 任務等【異步任務】按照先后順序插?到【任務隊列】中,等待執(zhí)?棧清空,再檢測本次的宏任務中注冊的所有微任務進行執(zhí)行清空。


- 所以正確的一次 Event loop 順序是這樣的
- 執(zhí)行同步代碼,這屬于宏任務
- 執(zhí)行棧為空,查詢本次是否有微任務需要執(zhí)行
- 執(zhí)行所有微任務
- 必要的話渲染 UI
- 然后開始下一輪
Event loop,執(zhí)行宏任務-微任務的循環(huán)
- 關(guān)于執(zhí)行棧
先進后出,后進先出。無限制的遞歸會導致執(zhí)行棧溢出,eg:
var i = 0;
function task(){
i++
console.log(`遞歸了${i}次`)
task()
}
task()

跨越遞歸限制:
var i = 0;
function task(){
i++
console.log(`遞歸了${i}次`)
//使?異步任務來阻?遞歸的溢出
setTimeout(function(){
task()
},0)
}
task()
使用異步任務進行調(diào)用,會先把異步內(nèi)容掛起,等待執(zhí)行棧清空再進行異步內(nèi)容的入棧執(zhí)行和出棧,依次無限循環(huán)
- 特殊任務
關(guān)于requestAnimationFrame函數(shù)的執(zhí)?頻率是每秒鐘60次左右,是根據(jù)瀏覽器相關(guān)的,所以和其他timer類的宏任務的執(zhí)行時機不確定,可能在之前可能在之后。
setTimeout(function() {console.log('timer1')}, 0)
requestAnimationFrame(function(){
console.log('UI update')
})
setTimeout(function() {console.log('timer2')}, 0)
new Promise(function executor(resolve) {
console.log('promise1')
resolve()
console.log('promise2')
}).then(function() {
console.log('promise.then')
})
console.log('end')
// 結(jié)果:
// promise1 promise2 end promise.then (UI update) timer1 timer2 (UI update)
關(guān)于事件
document.addEventListener('click', function(){
Promise.resolve().then(()=> console.log(1));
console.log(2);
})
document.addEventListener('click', function(){
Promise.resolve().then(()=> console.log(3));
console.log(4);
})
// 2 1 4 3
解析:當元素被點擊時,兩個事件會按照先后的注冊順序放?異步任務隊列中進?執(zhí)?,所以事件1和事件2 會按照代碼編寫的順序觸發(fā)。由于事件執(zhí)?時并不會阻斷JS默認代碼的運?,所以事件任務也是異步任務,并且是宏 任務,所以兩個事件相當于按順序執(zhí)?的兩個宏任務
即順序:同步代碼(掛起click事件異步代碼)-> 點擊觸發(fā) -> 執(zhí)行第一個異步代碼宏任務(其中先執(zhí)行同步代碼2再微任務1) -> 執(zhí)行第二個異步代碼宏任務(其中先執(zhí)行同步代碼4再微任務3)