Event Loop
const test=()=>{
console.log(1)
setTimeout(()=>{
console.log(2)
},0)
Promise.resolve().then(()=>{
console.log(3)
})
console.log(4)
}
test();
//1
//4
//3
//2
可以看出:
- Promise和setTimeout都是是異步
- Promise優(yōu)先級(jí)高于setTimeout
為什么呢~~我們先來熟悉下基本概念
執(zhí)行上下文 (execution context)
當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)个初,會(huì)創(chuàng)建一個(gè)活動(dòng)記錄(執(zhí)行上下文)猴蹂,這個(gè)記錄會(huì)包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)的調(diào)用方式晕讲、傳入的參數(shù)等信息马澈。
以下三種情況會(huì)分別創(chuàng)建上下文
- 全局執(zhí)行上下文:是為運(yùn)行代碼主體而創(chuàng)建的執(zhí)行上下文弄息,也就是說它是為那些存在于JavaScript 函數(shù)之外的任何代碼而創(chuàng)建的。
- 函數(shù)執(zhí)行上下文:每個(gè)函數(shù)會(huì)在執(zhí)行的時(shí)候創(chuàng)建自己的執(zhí)行上下文涤伐。這個(gè)上下文就是通常說的 “本地上下文”缨称。
- 使用 eval() 函數(shù)
執(zhí)行棧(call stack)
執(zhí)行棧(也稱為調(diào)用棧),是解釋器(比如瀏覽器中的 JavaScript 解釋器)追蹤函數(shù)執(zhí)行流的一種機(jī)制睦尽。當(dāng)執(zhí)行環(huán)境中調(diào)用了多個(gè)函數(shù)時(shí),通過這種機(jī)制山害,我們能夠追蹤到哪個(gè)函數(shù)正在執(zhí)行沿量,執(zhí)行的函數(shù)體中又調(diào)用了哪個(gè)函數(shù)。
- 每調(diào)用一個(gè)函數(shù)朴则,解釋器就會(huì)把該函數(shù)添加進(jìn)調(diào)用棧并開始執(zhí)行。
- 正在調(diào)用棧中執(zhí)行的函數(shù)還調(diào)用了其它函數(shù)汹想,那么新函數(shù)也將會(huì)被添加進(jìn)調(diào)用棧芥被,一旦這個(gè)函數(shù)被調(diào)用,便會(huì)立即執(zhí)行拴魄。
- 當(dāng)前函數(shù)執(zhí)行完畢后,解釋器將其清出調(diào)用棧夏漱,繼續(xù)執(zhí)行當(dāng)前執(zhí)行環(huán)境下的剩余的代碼顶捷。
- 當(dāng)分配的調(diào)用棧空間被占滿時(shí)服赎,會(huì)引發(fā)“堆棧溢出”錯(cuò)誤(如遞歸使用不當(dāng))
RangeError:Maximum call stack size exceeded
交播。
分析以下程序:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
當(dāng)上述代碼執(zhí)行時(shí)践付,Javascript引擎會(huì)創(chuàng)建 執(zhí)行上下文棧,每個(gè)代碼段開始執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)新的上下文來運(yùn)行它隧土,每個(gè)上下文創(chuàng)建時(shí)都會(huì)push
到棧中命爬,在代碼退出的時(shí)候從上下文棧中pop
,完成銷毀饲宛。
其大致流程:
- 程序開始運(yùn)行落萎,創(chuàng)建全局執(zhí)行上下文并壓入執(zhí)行棧中
- 當(dāng)執(zhí)行到
first()
時(shí)炭剪,會(huì)為該函數(shù)創(chuàng)建函數(shù)執(zhí)行上下文,并push
到棧中 - 當(dāng)
first()
調(diào)用second()
,創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文,并push
到棧中 - 當(dāng)
second()
執(zhí)行完畢奴拦,將其上下文從棧中彈出并銷毀,同時(shí)從棧中取棧頂?shù)纳舷挛牟⒒謴?fù)執(zhí)行绿鸣,也就是執(zhí)行程序剩余部分 -
first()
執(zhí)行完畢后暂氯,其上下文中棧中彈出并銷毀 - 程序結(jié)束,全局執(zhí)行上下文從執(zhí)行棧中彈出并銷毀
通過執(zhí)行棧機(jī)制痴施,每個(gè)程序和函數(shù)都有自己對(duì)應(yīng)的上下文,每一個(gè)上下文中都能夠跟蹤程序中的下一行需要執(zhí)行的代碼以及相應(yīng)的上下文信息动遭,方便我們調(diào)試程序神得,追蹤異常。
眾所周知哩簿,Javascript是單線程酝静,也就意味著一個(gè)call stack,同一時(shí)間只執(zhí)行一件事羡玛。
而我們?cè)谕綀?zhí)行一些耗時(shí)代碼片段時(shí),會(huì)阻塞(block)當(dāng)前的線程亿遂,在這個(gè)過程中渺杉,CPU是閑置的,為了充分利用資源是越,避免競(jìng)態(tài)條件出現(xiàn),這一類任務(wù)被設(shè)計(jì)成允許暫時(shí)掛起浦徊,等到有了結(jié)果再執(zhí)行的任務(wù)天梧,從而引入異步。
任務(wù)隊(duì)列(task queue)
在執(zhí)行棧中冕香,如果遇到異步函數(shù)后豫,如setTimeout()
會(huì)交給指定模塊處理,然后繼續(xù)執(zhí)行同步代碼挫酿。而當(dāng)異步函數(shù)達(dá)到觸發(fā)條件時(shí),會(huì)根據(jù)函數(shù)類型惫霸,壓入指定的任務(wù)隊(duì)列拄衰。
有兩類任務(wù)隊(duì)列:宏任務(wù)隊(duì)列(macro tasks)和微任務(wù)隊(duì)列(micro tasks)。宏任務(wù)隊(duì)列可以有多個(gè)翘悉,微任務(wù)隊(duì)列只有一個(gè)。
返回剛才的例子老赤,當(dāng)函數(shù)test()
調(diào)用,執(zhí)行到setTimeout()
和Promise()
時(shí)抬旺,會(huì)將其放入任務(wù)隊(duì)列,繼續(xù)執(zhí)行函數(shù)的剩余部分知道出棧銷毀其上下文汉柒。當(dāng)stack空時(shí)责鳍,從任務(wù)隊(duì)列中取任務(wù)來執(zhí)行,但是因?yàn)g覽器和Node的event loop機(jī)制不同正塌,所以分別對(duì)其分析恤溶。
瀏覽器環(huán)境中的Event Loop
基于HTML5標(biāo)準(zhǔn)中的Event Loop,其Event Loop 中的異步任務(wù)分到兩個(gè)隊(duì)列中咒程。
macrotask queue 即我們所說的任務(wù)隊(duì)列。一個(gè)事件循環(huán)中有一個(gè)或多個(gè)任務(wù)隊(duì)列孵坚,其實(shí)并不是 queue
,而是set
,因?yàn)槭录h(huán)處理模型的第一步從選定的隊(duì)列中獲取第一個(gè)可運(yùn)行任務(wù)忧饭,而不是使第一個(gè)任務(wù)出隊(duì)。
microtask queue 并不真正的任務(wù)隊(duì)列刺洒。一個(gè)事件循環(huán)中只有一個(gè)microtask queue
一個(gè)任務(wù)可以被放入到macrotask隊(duì)列吼砂,也可以放入microtask隊(duì)列
回到最開始的例子test()
,其執(zhí)行過程:
- 程序開始渔肩,創(chuàng)建
全局執(zhí)行上下文
,壓入棧中干跛。 - 開始調(diào)用
test()
,創(chuàng)建其函數(shù)執(zhí)行上下文
,壓入棧中 - 執(zhí)行到
setTimeout(cb)
時(shí)铐炫,創(chuàng)建其上下文胡嘿,并壓入棧中钳踊,因?yàn)椴皇峭胶瘮?shù),交由webapi
進(jìn)行處理并出棧銷毀其上下文(在0s后逢享,壓入macrotask queue)吴藻,繼續(xù)執(zhí)行下一個(gè)代碼片段。 - 執(zhí)行到
Promise(cb)
時(shí)沟堡,創(chuàng)建其上下文,并壓入棧中禀横,因?yàn)椴皇峭胶瘮?shù)粥血,交由webapi
進(jìn)行處理并出棧銷毀其上下文(并壓入microtask queue),繼續(xù)執(zhí)行下一個(gè)代碼片段复亏。 - 函數(shù)
test()
執(zhí)行完畢缔御,彈出其上下文,并銷毀 - 程序執(zhí)行完畢耕突,銷毀全局執(zhí)行上下文
- 從
microtask queue
中取出Promise任務(wù),執(zhí)行其回調(diào)函數(shù)cb - 再從
macrotask queue
中取出setTimeout任務(wù)炕泳,執(zhí)行其回調(diào)函數(shù)cb
那么加入U(xiǎn)I Rendering呢上祈?
let test2=()=>{
console.log('process start');
setTimeout(()=>{
console.log('processing setTimeout')
},100)
let dom=document.getElementById('app')
dom.style.backgroundColor="red"
Promise.resolve().then(()=>{
console.log("processing promise")
})
console.log('process end')
}
test2();
作為腳本的一部分浙芙,程序必須執(zhí)行完成荤懂,瀏覽器才會(huì)執(zhí)行渲染。
Event Loop可以保證任務(wù)在下一次渲染前執(zhí)行完成晤锥。
我們可以使用
requestAnimationFrame
(RAF回調(diào)) 廊宪,會(huì)以16.6ms的頻率執(zhí)行
setTimeout實(shí)際會(huì)多出4ms左右的延時(shí)
那么完整的Event Loop 流程如下:
- 從宏任務(wù)隊(duì)列出列并執(zhí)行最前面的任務(wù)(比如“script”)。
- 調(diào)用棧為空箭启,檢查microtask queue
- 執(zhí)行microtask隊(duì)列,按照隊(duì)列 先進(jìn)先出 的原則放妈,執(zhí)行完所有microtask隊(duì)列任務(wù)荐操;
- 有需要執(zhí)行渲染(在一幀以內(nèi)的多次Dom變動(dòng)瀏覽器不會(huì)立即響應(yīng),而是會(huì)積攢變動(dòng)以最高60HZ的頻率更新視圖)宅倒。
- 執(zhí)行任務(wù)隊(duì)列屯耸,如果任務(wù)隊(duì)列不為空,取出任務(wù)隊(duì)列中第一個(gè)可運(yùn)行的宏任務(wù)疗绣,執(zhí)行完畢后,檢查
microtask queue
;而如果任務(wù)隊(duì)列為空灶搜,則直接檢查microtask queue
注意事項(xiàng):
- microtask 工窍,一直執(zhí)行前酿,直到隊(duì)列為空,但是如果過程中有新的任務(wù)加進(jìn)來淹仑,且添加的速度比執(zhí)行快,那么就會(huì)永遠(yuǎn)執(zhí)行微任務(wù)匀借,從而導(dǎo)致阻塞Event Loop。
- macrotask ,每次執(zhí)行一個(gè)任務(wù)凳怨,如果有新的任務(wù)是鬼,就添加到隊(duì)列尾部。
- animation cb均蜜,一直執(zhí)行囤耳,直到隊(duì)列中的所有任務(wù)完成,如果動(dòng)畫回調(diào)中又有動(dòng)畫回調(diào)充择,它們會(huì)在下一幀執(zhí)行
-
new Promise
構(gòu)造函數(shù)內(nèi)部是同步執(zhí)行
總結(jié):
- 調(diào)用棧清空時(shí)會(huì)立刻先處理所有微任務(wù)隊(duì)列中的事件,然后再去宏任務(wù)隊(duì)列中取出一個(gè)事件化焕。同一次事件循環(huán)中铃剔,微任務(wù)永遠(yuǎn)在宏任務(wù)之前執(zhí)行。
- 本質(zhì)上來說 在一個(gè)事件循環(huán)中凤类,Microtask的執(zhí)行方式基本上就是用同步的
- 當(dāng)引擎處理任務(wù)時(shí)不會(huì)執(zhí)行渲染普气。如果執(zhí)行需要很長(zhǎng)一段時(shí)間也是如此。對(duì)于 DOM 的修改只有當(dāng)任務(wù)執(zhí)行完成才會(huì)被繪制
宏任務(wù)和微任務(wù)的區(qū)別
宏任務(wù):由事件回調(diào)现诀、程序啟動(dòng)或觸發(fā)間隔運(yùn)行的任何JavaScript代碼,包括解析HTML,生成DOM坐桩,執(zhí)行主線程JS代碼以及其他事件封锉,例如頁面加載膘螟,輸入碾局,網(wǎng)絡(luò)事件,計(jì)時(shí)器事件等内斯。從瀏覽器的角度來看蚯瞧,Macrotasks代表了一些離散且獨(dú)立的工作。
微任務(wù):只是一個(gè)簡(jiǎn)短的函數(shù)(因此得名)埋合,它在創(chuàng)建函數(shù)退出后執(zhí)行,是完成一些次要任務(wù)來更新應(yīng)用程序狀態(tài),例如處理Promise的回調(diào)和DOM修改蜜猾,以便可以在重新渲染瀏覽器之前執(zhí)行這些任務(wù)振诬。微任務(wù)應(yīng)盡快異步執(zhí)行,因此其成本低于Macrotask肩豁,它可使我們?cè)赨I呈現(xiàn)之前再次執(zhí)行辫呻,從而避免不必要的UI呈現(xiàn)。
問題
// 同步
[1,2,3,4].forEach((i)=>{
console.log(i);
})
// 異步
function asyncForEach(arr,cb){
arr.forEach((i)=>{
setTimeout(()=>{
cb(i)
},0)
})
}
asyncForEach(['a','b','c','d'],(i)=>{
console.log(i)
})
microtask的執(zhí)行機(jī)制并不是穩(wěn)定的祟昭,實(shí)際上是因調(diào)用棧的情況而有所不同
btn.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('microtask 1'))
console.log('Listener 1')
})
btn.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('microtask 2'))
console.log('Listener 2');
})
// Listener 1
// microtask 1
// Listener 2
// Listener 2
- 點(diǎn)擊時(shí)怖侦,觸發(fā)第一個(gè)cb,將Promise訪入microtask
- 執(zhí)行
console.log('Listener 1')
- 此時(shí)調(diào)用棧為空匾寝,執(zhí)行第一個(gè)microtask,即
console.log('microtask 1')
- 同樣的踩萎,觸發(fā)第二個(gè)cb很钓。。企孩。袁稽。
但是如果是js 觸發(fā)的click()
呢
btn.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('microtask 1'))
console.log('Listener 1')
})
btn.addEventListener('click',()=>{
Promise.resolve().then(()=>console.log('microtask 2'))
console.log('Listener 2');
})
btn.click();
//?
- 首先棧中執(zhí)行click()
- 事件調(diào)度,將執(zhí)行第一個(gè)cb,將Promise訪入microtask,執(zhí)行
console.log('Listener 1')
补疑,銷毀監(jiān)聽器 - 時(shí)間調(diào)度歹撒,執(zhí)行第二個(gè)cb,并將Promise訪入microtask,執(zhí)行
console.log('Listener 2')
,銷毀監(jiān)聽器 - 調(diào)用棧中彈出
click
上下文并銷毀 - 開始執(zhí)行microtask暖夭,首先是
console.log('microtask 1')
,接著是console.log('microtask 2')
現(xiàn)實(shí)場(chǎng)景中,如果我們?cè)趫?zhí)行自動(dòng)化測(cè)試時(shí)竭望,通過腳本控制執(zhí)行事件裕菠,就可能會(huì)導(dǎo)致結(jié)果的差異。
保證條件性使用 promises 時(shí)的順序
https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide
Node.js的Event Loop
NodeJs是以非阻塞的I/O單線程旧烧,實(shí)現(xiàn)主要依賴于[libuv](http://docs.libuv.org/en/v1.x/design.html)
libuv,由C語言編寫的事件驅(qū)動(dòng)庫萤彩,是NodeJs異步編程的基礎(chǔ),屬于底層I/O引擎雀扶。主要負(fù)責(zé)Node API的執(zhí)行,將不同的任務(wù)分配給不同的線程予权,從而形成了Node Event Loop浪册,以異步的方式將執(zhí)行結(jié)果返回給V8引擎
NodeJs Event Loop 的運(yùn)行是這樣的:
- timers:執(zhí)行timer(setTimeout/setInterval)回調(diào)
- pending callbacks:執(zhí)行系統(tǒng)操作的回調(diào)--內(nèi)部
- idle, prepare:執(zhí)行空閑/準(zhǔn)備句柄回調(diào)-內(nèi)部使用
- poll:等待新I/O事件
- check:執(zhí)行setImmediate回調(diào)
- close callbacks :關(guān)閉回調(diào)--內(nèi)部執(zhí)行
- 每個(gè)階段都會(huì)有一個(gè)callbacks的先進(jìn)先出的隊(duì)列執(zhí)行村象。
- 當(dāng)event loop 運(yùn)行到一個(gè)指定階段時(shí)攒至,該階段的fifo隊(duì)列將被執(zhí)行躁劣,當(dāng)隊(duì)列執(zhí)行完或執(zhí)行的callbacks數(shù)量超過該階段的上線時(shí),轉(zhuǎn)入下一階段
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Node.js的Event Loop過程
- 執(zhí)行全局Script的同步任務(wù)
- 執(zhí)行microtask微任務(wù)志膀,先執(zhí)行所有Next Tick Queue(process.nextTick)中的所有任務(wù)鳖擒,再執(zhí)行 Other Microtask queue中的所有任務(wù)
- 開始執(zhí)行macrotask宏任務(wù),共6個(gè)階段戳稽,從第一個(gè)階段開始執(zhí)行對(duì)應(yīng)階段的macrotask queue中的所有任務(wù)
- Timer Queue->步驟2->I/O Queue ->步驟2->Check Queue->Close Callback Queue...
Poll階段細(xì)節(jié)
Poll階段的兩個(gè)主要的功能:
- 計(jì)算應(yīng)該被block多久
- 處理poll隊(duì)列的事件
主要流程:
- 檢測(cè)poll隊(duì)列如果為空/達(dá)到閾值圆裕,繼續(xù)第2步;否則執(zhí)行第3步
- 如果設(shè)置了
setImmediate()
,進(jìn)入check階段吓妆,否則等待回調(diào)添加到隊(duì)列行拢,立即執(zhí)行 - 同步執(zhí)行poll隊(duì)列中的回調(diào)
- 如果poll隊(duì)列為空,檢查是否有到達(dá)時(shí)間的timer舟奠,有則執(zhí)行timers回調(diào);否則繼續(xù)等在callback加入poll隊(duì)列
練習(xí)
Promise.resolve(123).then((res)=>console.log(res))
process.nextTick(()=>console.log(456))
解釋:
這里process.nextTick()
是一個(gè)異步的node Api抬纸,但不屬于event loop的階段耿戚,調(diào)用時(shí)會(huì)中斷event loop優(yōu)先執(zhí)行process.nextTick的回調(diào)。
setTimeout(()=>console.log('setTimeout'),0)
setImmediate(()=>console.log('setImmediate'))
解釋:
setImmediate()
用于中斷長(zhǎng)時(shí)間運(yùn)行的操作坛猪,并在完成其他操作后立即執(zhí)行其回調(diào)皂股。
setImmediate()
和setTimeout()
執(zhí)行順序不固定,取決于node的準(zhǔn)備時(shí)間。
setTimeout()
和setInterval()
的第二個(gè)參數(shù)的取值范圍是[1,2^32-1]
悍募,如果超過這個(gè)范圍战转,初始化為1,即setTimeout(fn,0)===setTimeout(fn,1)
槐秧。
我們知道setTimeout
的回調(diào)函數(shù)在timer階段執(zhí)行忧设,setImmediate
的回調(diào)函數(shù)在check階段執(zhí)行址晕,event loop 的開始會(huì)檢查timer階段,但是在開始之前timer階段會(huì)消耗一定的時(shí)間谨垃;
- timer前的準(zhǔn)備時(shí)間超過1ms,滿足loop->time>=1,則執(zhí)行timer階段(setTimeout)的回調(diào)函數(shù)
- timer前的準(zhǔn)備時(shí)間小于1ms胳赌,則先自行check階段(setImmediate)的回調(diào)函數(shù)匙隔,下一次event loop再次開始執(zhí)行timer階段(setTimeout)的回調(diào)函數(shù)
如果我們想確保先執(zhí)行setTimeout
的回調(diào)
setTimeout(()=>console.log('setTimeout'),0)
setImmediate(()=>console.log('setImmediate'))
const start=new Date()
//睡眠10ms
while (Date.now()-start<10);
那如果我們想先執(zhí)行setImmediate
的回調(diào)呢?從正常的event loop開始一定是先執(zhí)行timer的回調(diào)捍掺,如果我們可以在pending callbacks->idea/prepare->pool這個(gè)階段內(nèi)開始觸發(fā)setTimeout
再膳,那么就可以先執(zhí)行setImmediate
的回調(diào)了。
const fs=require('fs')
fs.readFile(__dirname,()=>{
setTimeout(()=>console.log('setTimeout'),0);
setImmediate(()=>console.log('setImmediate'));
})
上段代碼中喂柒,我們將event loop 的起始階段放在了 poll階段,等待i/o湃番,然后就會(huì)先執(zhí)行setImmediate
的回調(diào)吭露。
瀏覽器與Node區(qū)別
- 瀏覽器端,一次執(zhí)行一個(gè)宏任務(wù)泥兰,兩個(gè)宏任務(wù)間隔內(nèi)執(zhí)行微任務(wù)隊(duì)列中的所有微任務(wù)
- Node端,一個(gè)階段執(zhí)行當(dāng)前宏任務(wù)隊(duì)列中的所有宏任務(wù)鞋诗,每個(gè)階段間隔輪詢微任務(wù)隊(duì)列中的微任務(wù)
NodeV11發(fā)生了變化
setTimeout(() => console.log('timeout1'));
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(() => console.log('promise resolve'))
});
setTimeout(() => console.log('timeout3'));
setTimeout(() => console.log('timeout4'));
請(qǐng)分別在瀏覽器、Node V10全庸,Node V11 運(yùn)行以上代碼
MacroTask and MicroTask execution order
待學(xué)習(xí)
從event loop規(guī)范探究javaScript異步及瀏覽器更新渲染時(shí)機(jī)
【轉(zhuǎn)向Javascript系列】深入理解Web Worker
Web Worker淺識(shí)
In depth: Microtasks and the JavaScript runtime environment
問題
Node.js v10.15.3版本
https://github.com/nodejs/node/issues/27747