作為前端工程師技俐,大家都知道js是前端一開始就要學(xué)會(huì)的知識(shí)點(diǎn),js的代碼你會(huì)寫了统台,那js的運(yùn)行機(jī)制你了解嗎雕擂?只有了解了js的運(yùn)行機(jī)制,才能在工作中如魚得水贱勃,今天就跟隨珠峰的老師一起來了解下js的運(yùn)行機(jī)制吧井赌。
JavaScript單線程模型
JavaScript是單線程的,JavaScript只在一個(gè)線程上運(yùn)行贵扰,但是瀏覽器是多線程的仇穗,典型的瀏覽器有如下線程:
JavaScript引擎線程
GUI渲染線程
瀏覽器事件觸發(fā)線程
瀏覽器Http請求線程
JavaScript為什么是單線程的
JavaScript之所以采用單線程 而不是多線程,由于作為瀏覽器腳本語言戚绕,主要用途是與用戶互動(dòng)纹坐,以及操作DOM(文檔對象模型)和BOM(瀏覽器對象模型), 而多線程需要共享資源列肢,多線程編程經(jīng)?常面臨鎖恰画、狀態(tài)同步等問題宾茂。
假定JavaScript同時(shí)有兩個(gè)線程,這兩個(gè)線程同時(shí)操作同一個(gè)DOM增刪修改操作拴还,這時(shí)瀏覽器應(yīng)該以哪個(gè)線程操作為準(zhǔn)跨晴?無疑會(huì)帶來同步問題。
既然JavaScript是單線程的片林,這就意味著端盆,一次只能運(yùn)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待费封。
為了利用多核CPU的計(jì)算能力焕妙,HTML5提出了Web Worker,它會(huì)在當(dāng) ? 前JavaScript的執(zhí)行主線程中利用Worker類新開辟一個(gè)額外的線程來加載和運(yùn)行特定的JavaScript文件弓摘,但在HTML5 Web Worker中是不能操作DOM的焚鹊,任何需要操作DOM的任務(wù)都需要委托給JavaScript主線程來執(zhí)行,所以雖然引入HTML5 Web Worker韧献,但仍然沒有改變JavaScript單線程的本質(zhì)末患。
任務(wù)列表
Javascript有一個(gè)main thread 主進(jìn)程和call-stack(一個(gè)調(diào)用堆棧),在對一個(gè)調(diào)用堆棧中的task處理的時(shí)候锤窑,其他的都要等著璧针。當(dāng)在執(zhí)行過程中遇到一些類似于setTimeout等異步操作的時(shí)候,會(huì)交給瀏覽器的其他模塊(以webkit為例渊啰,是webcore模塊)進(jìn)行處理探橱,當(dāng)?shù)竭_(dá)setTimeout指定的延時(shí)執(zhí)行的時(shí)間之后,task(回調(diào)函數(shù))會(huì)放入到任務(wù)隊(duì)列之中绘证。
一般不同的異步任務(wù)的回調(diào)函數(shù)會(huì)放入不同的任務(wù)隊(duì)列之中隧膏。等到調(diào)用棧中所有task執(zhí)行完畢之后,接著去執(zhí)行任務(wù)隊(duì)列之中的task(回調(diào)函數(shù))嚷那。
異步和同步
一般而言私植,操作分為:發(fā)出調(diào)用和得到結(jié)果兩步。
同步:
同步是指车酣,發(fā)出調(diào)用,但無法立即得到結(jié)果索绪,需要一直等待湖员,直到返回結(jié)果。同步任務(wù)會(huì)進(jìn)入主線程, 主線程后面任務(wù)必須要等當(dāng)前任務(wù)執(zhí)行完才能執(zhí)行瑞驱,從而導(dǎo)致主線程阻塞娘摔。
異步:
異步是指,調(diào)用之后唤反,不能直接拿到結(jié)果凳寺,通過event loop事件處理機(jī)制鸭津,在Event Queue注冊回調(diào)函數(shù)最終拿到結(jié)果(拿到結(jié)果中間的時(shí)間可以介入其他任務(wù))。
JavaScript是如何工作的肠缨,首先理解幾個(gè)概念
JS Engine(JS引擎)
Runtime(運(yùn)行上下文)
Call Stack(調(diào)用棧)
Event Loop(事件循環(huán))
Callback(回調(diào))
JS Engine
JavaScript引擎就是用來執(zhí)行JS代碼的, 通過編譯器將代碼編譯成可執(zhí)行的機(jī)器碼讓計(jì)算機(jī)去執(zhí)行(Java中的JVM虛擬機(jī)一樣)逆趋。
常見的JavaScript虛擬機(jī)(一般也把虛擬機(jī)稱為引擎):
Chakra(Microsoft Internet Explorer)
Nitro/JavaScript Core (Safari)
Carakan (Opera)
SpiderMonkey (Firefox)
V8 (Chrome, Chromium)
目前比較流行的就是V8引擎,Chrome瀏覽器和Node.js采用的引擎就是V8引擎晒奕。
引擎主要由堆(Memory Heap)和棧(Call Stack)組成
Heap(堆) - JS引擎中給對象分配的內(nèi)存空間是放在堆中的
Stack(棧)- 這里存儲(chǔ)著JavaScript正在執(zhí)行的任務(wù)闻书。每個(gè)任務(wù)被稱為幀(stack of frames)。
主線程運(yùn)行的時(shí)候脑慧,產(chǎn)生堆(heap)和棧(stack),棧中的代碼調(diào)用個(gè)各種外部api魄眉。
RunTime (運(yùn)行環(huán)境)
JS在瀏覽器環(huán)境中運(yùn)行時(shí),BOM和DOM對象提供了很多相關(guān)外部接口(這些接口不是V8引擎提供的)闷袒,供JS運(yùn)行時(shí)調(diào)用坑律,以及JS的事件循環(huán)(Event Loop)和事件隊(duì)列(Callback Queue),把這些稱為RunTime囊骤。在Node.js中晃择,可以把Node的各種庫提供的API稱為RunTime
Call Stack
當(dāng)JavaScript代碼執(zhí)行的時(shí)候,創(chuàng)建執(zhí)行環(huán)境是很重要的淘捡,它可能是下面三種情況中的一種:
全局 code(Global code)——代碼第一次執(zhí)行的默認(rèn)環(huán)境
函數(shù) code(Function code)——執(zhí)行流進(jìn)入函數(shù)體
Eval code(Eval code)——代碼在eval函數(shù)內(nèi)部執(zhí)行
JavaScript代碼首次被載入時(shí)藕各,會(huì)創(chuàng)建一個(gè)全局上下文,當(dāng)調(diào)用一個(gè)函數(shù)時(shí)焦除,會(huì)創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文激况。
在計(jì)算機(jī)系統(tǒng)中棧是一種遵從先進(jìn)后出(FILO)原則的區(qū)域。函數(shù)被調(diào)用時(shí)膘魄,創(chuàng)建一個(gè)新的執(zhí)行環(huán)境乌逐,就會(huì)被加入到執(zhí)行棧頂部,瀏覽器始終執(zhí)行當(dāng)前在棧頂部的執(zhí)行環(huán)境创葡。一旦函數(shù)完成了當(dāng)前的執(zhí)行環(huán)境浙踢,它就會(huì)被彈出棧的頂部, 把控制權(quán)返回給當(dāng)前執(zhí)行環(huán)境的下個(gè)執(zhí)行環(huán)境。
案例:瀏覽器第一次加載你的script灿渴,它默認(rèn)的進(jìn)了全局執(zhí)行環(huán)境洛波,然后main執(zhí)行創(chuàng)建一個(gè)新的執(zhí)行環(huán)境,把它添加到已經(jīng)存在的執(zhí)行棧的頂部骚露,在里面執(zhí)行Student構(gòu)造函數(shù)蹬挤,執(zhí)行流進(jìn)入內(nèi)部函數(shù) 將生成執(zhí)行環(huán)境添加到當(dāng)前棧頂,在Student構(gòu)造函數(shù)里棘幸,又調(diào)用sayHi方法焰扳,再次把sayHi生成執(zhí)行環(huán)境壓入到棧頂。當(dāng)函數(shù)執(zhí)行完一次彈出棧頂。
classStudent{
constructor(age,?name)?{
this.name?=?name;
this.age?=?age;
this.sayName();//?stack?3
}
sayName()?{
console.log(`my?name?is${this.name},?this?year?age?is${this.age}`);
}
}
functionmain(age,?name){
newStudent(age,?name);//?stack?2
}
main(23,'John');//?stack?1
程序運(yùn)行時(shí)吨悍,首先main()函數(shù)的執(zhí)行上下文入棧扫茅,再調(diào)用Student構(gòu)造函數(shù)添加到當(dāng)前棧尾,在Student里再調(diào)用sayName()方法育瓜,添加到此時(shí)棧尾葫隙。最終main方法所在的位置叫棧底,sayName方法所在的位置是棧頂爆雹,層層調(diào)用停蕉,直至整個(gè)調(diào)用棧完成返回結(jié)果,最后再由棧頂依次出棧钙态。
Event Loop & Callback
Event Loop 類似于一個(gè)while(true)的循環(huán)慧起,每執(zhí)行一次循環(huán)體的過程我們成為Tick。每個(gè)Tick的過程就是查看是否有事件待處理册倒,當(dāng)Call Stack里面的調(diào)用棧運(yùn)行完變成空了蚓挤,就取出事件及其相關(guān)的回調(diào)函數(shù)。放到調(diào)用棧中并執(zhí)行它驻子。
調(diào)用棧中遇到DOM操作灿意、ajax請求以及setTimeout等WebAPIs的時(shí)候就會(huì)交給瀏覽器內(nèi)核的其他模塊進(jìn)行處理,webkit內(nèi)核在Javasctipt執(zhí)行引擎之外崇呵,有一個(gè)重要的模塊是webcore模塊缤剧。對于圖中WebAPIs提到的三種API,webcore分別提供了DOM Binding域慷、network荒辕、timer模塊來處理底層實(shí)現(xiàn)。
等到這些模塊處理完這些操作的時(shí)候?qū)⒒卣{(diào)函數(shù)放入任務(wù)隊(duì)列中犹褒,之后等棧中的task執(zhí)行完之后再去執(zhí)行任務(wù)隊(duì)列之中的回調(diào)函數(shù)抵窒。
Javascript有一個(gè)main thread 主進(jìn)程和call-stack(一個(gè)調(diào)用堆棧),在對一個(gè)調(diào)用堆棧中的task處理的時(shí)候叠骑,其他的都要等著李皇。當(dāng)在執(zhí)行過程中遇到一些類似于setTimeout等異步操作的時(shí)候,會(huì)交給瀏覽器的其他模塊(以webkit為例宙枷,是webcore模塊)進(jìn)行處理掉房,當(dāng)?shù)竭_(dá)setTimeout指定的延時(shí)執(zhí)行的時(shí)間之后,task(回調(diào)函數(shù))會(huì)放入到任務(wù)隊(duì)列之中慰丛。
一般不同的異步任務(wù)的回調(diào)函數(shù)會(huì)放入不同的任務(wù)隊(duì)列之中圃阳。等到調(diào)用棧中所有task執(zhí)行完畢之后,接著去執(zhí)行任務(wù)隊(duì)列之中的task(回調(diào)函數(shù))璧帝。
代碼案例:
console.log('Hi');
setTimeout(functioncb1(){
console.log('cb1');
},5000);
console.log('Bye');
以上代碼從上到下 首先執(zhí)行l(wèi)og('Hi') 它是一個(gè)普通方法立即被執(zhí)行,當(dāng)遇到定時(shí)器的時(shí)候富寿,執(zhí)行引擎將其添加到調(diào)用棧睬隶,調(diào)用棧發(fā)現(xiàn)setTimeout是WebAPIs中的API锣夹,將其出棧交給瀏覽器的timer模塊進(jìn)行處理,此時(shí)timer模塊去處理延遲執(zhí)行的函數(shù)苏潜,此時(shí)執(zhí)行l(wèi)og('Bye'),輸出'Bye'银萍,當(dāng)timer模塊中延時(shí)方法規(guī)定的時(shí)間到了之后就將其放入到任務(wù)隊(duì)列之中,此時(shí)調(diào)用棧中的task已經(jīng)全部執(zhí)行完畢恤左。
調(diào)用棧中的task執(zhí)行完畢之后贴唇,執(zhí)行引擎會(huì)接著看執(zhí)行任務(wù)隊(duì)列中是否有需要執(zhí)行的回調(diào)函數(shù)。
Event Loop處理機(jī)制
什么是Event Loop飞袋?
Event Loop(事件循環(huán))是實(shí)現(xiàn)異步的一種機(jī)制戳气,允許 Node.js 執(zhí)行非阻塞 I/O 操作 。
大多數(shù)現(xiàn)代的系統(tǒng)內(nèi)核都是多線程的, 他們在后臺(tái)可以處理多個(gè)同時(shí)執(zhí)行的操作. 當(dāng)其中一個(gè)操作完成時(shí), 系統(tǒng)內(nèi)核會(huì)通知Node.js, 然后與之相關(guān)的回調(diào)函數(shù)會(huì)被加入到 poll隊(duì)列 并且最終被執(zhí)行巧鸭。
注意: 在Windows和Unix/Linux實(shí)現(xiàn)之間存在一點(diǎn)小小的差異, 但對本示例來說這并不重要瓶您,最重要的部分都已列在這里了,實(shí)際上有7或8個(gè)階段, 但我們關(guān)心的和Node.js實(shí)際會(huì)用到的階段都已經(jīng)列在了上面纲仍。
每個(gè)階段都有一個(gè)先進(jìn)先出(FIFO)的隊(duì)列呀袱,里面存放著要執(zhí)行的回調(diào)函數(shù),然而每個(gè)階段都有其特殊之處郑叠,當(dāng)事件循環(huán)進(jìn)入了某個(gè)階段后夜赵,它可以執(zhí)行該階段特有的任意操作,然后進(jìn)行該階段的任務(wù)隊(duì)列中的回調(diào)函數(shù)乡革,一直到隊(duì)列為空或已執(zhí)行回調(diào)的數(shù)量達(dá)到了允許的最大值寇僧,當(dāng)隊(duì)列為空或已執(zhí)行回調(diào)的數(shù)量達(dá)到了允許的最大值時(shí),事件循環(huán)會(huì)進(jìn)入下一個(gè)階段,階段之間會(huì)互相轉(zhuǎn)換署拟,循環(huán)順序并不是完全固定的 婉宰,因?yàn)楹芏嚯A段是由外部的事件觸發(fā)的。
階段概覽
timers(定時(shí)器):此階段執(zhí)行由setTimeout()和setInterval() 調(diào)度的回調(diào)函數(shù)
I/O callbacks(I/O回調(diào)): 此階段會(huì)執(zhí)行幾乎所有的回調(diào)函數(shù), 除了 close callbacks(關(guān)閉回調(diào)) 和 那些由 timers 與 setImmediate() 調(diào)度的回調(diào).
idle(空閑)推穷,prepare(預(yù)備): 此階段只在內(nèi)部調(diào)用
poll(輪詢): 檢索新的I/O事件心包,在恰當(dāng)?shù)臅r(shí)候會(huì)阻塞在這個(gè)階段
check(檢查): setImmediate() 設(shè)置的回調(diào)會(huì)在此階段被調(diào)用
close callbacks(關(guān)閉事件的回調(diào)): 諸如 socket.on('close', ...) 此類的回調(diào)在此階段被調(diào)用
在事件循環(huán)的每次運(yùn)行之間,Node.js會(huì)檢查它是否在等待異步I/O或定時(shí)器, 如果沒有的話就會(huì)自動(dòng)關(guān)閉馒铃。
一次事件循環(huán)就是處理以上幾個(gè)phase的過程蟹腾,此外還有兩個(gè)比較特殊的隊(duì)列Next Ticks Queue和Other Microtasks Queue,那另外兩個(gè)特殊的隊(duì)列是在什么時(shí)候運(yùn)行的呢区宇?
答案: 就是在每個(gè) phase運(yùn)行完后馬上就檢查這兩個(gè)隊(duì)列有無數(shù)據(jù)娃殖,有的話就馬上執(zhí)行這兩個(gè)隊(duì)列中的數(shù)據(jù)直至隊(duì)列為空。當(dāng)這兩個(gè)隊(duì)列都為空時(shí)议谷,event loop 就會(huì)接著執(zhí)行下一個(gè)phase炉爆。
這兩個(gè)隊(duì)列相比,Next Ticks Queue的權(quán)限要比Other Microtasks Queue的權(quán)限要高,因此Next Ticks Queue會(huì)先執(zhí)行芬首。
兩個(gè)比較特殊的隊(duì)列:
Next Ticks Queue: 保存process.nextTick中的回調(diào)函數(shù)
Other Microtasks Queue: 保存promise等microtask中的回調(diào)函數(shù)赴捞。
階段詳情
由于這些操作中的任意一個(gè)都可以調(diào)度更多的操作, 在 poll(輪詢) 階段處理的新事件被系統(tǒng)內(nèi)核加入隊(duì)列, 當(dāng)輪詢事件正在被處理時(shí)新的輪詢事件也可以被加入隊(duì)列. 因此, 長時(shí)間運(yùn)行的回調(diào)函數(shù)可以讓 poll 階段運(yùn)行的時(shí)間比 timer(計(jì)時(shí)器) 的閾值長得多。 看下面timer 和 poll 部分了解更多細(xì)節(jié)郁稍。
timers
給一個(gè)定時(shí)器(setTimeout/setInterval)指定時(shí)間閾值時(shí)赦政,給定的回調(diào)函數(shù)有時(shí)并不是在精確的時(shí)間閾值點(diǎn)執(zhí)行,定時(shí)器的閾值只是說 至少在這個(gè)時(shí)間閾值點(diǎn)執(zhí)行耀怜,然而操作系統(tǒng)調(diào)度或其他回調(diào)的執(zhí)行可能會(huì)延遲定時(shí)器回調(diào)的執(zhí)行恢着。
注意:從技術(shù)來講, poll階段會(huì)控制定時(shí)器何時(shí)被執(zhí)行
constfs?=require('fs');
//?設(shè)定一個(gè)100ms執(zhí)行的定時(shí)器
conststartTime?=Date.now();
setTimeout(()=>{
console.log('timeout延遲執(zhí)行時(shí)間',Date.now()?-?startTime);
console.log('timer');
},100);
//?異步讀取文件?假設(shè)95ms完成讀取任務(wù)
fs.readFile('./1.txt',?(err,?data)?=>?{//?回調(diào)函數(shù)中又耗費(fèi)100毫秒
conststartTime?=Date.now();
while(Date.now()?-?startTime?<200)?{
//?console.log(Date.now()?-?startTime);
}
});
開始事件循環(huán)定時(shí)器被加入到timer中延遲執(zhí)行财破,當(dāng)事件循環(huán)進(jìn)入poll階段掰派,它有一個(gè)隊(duì)列執(zhí)行I/O操作(fs.readFile())還未完成,poll階段將會(huì)阻塞狈究,大約95ms 完成了I/O操作(文件讀韧胩省),將要耗時(shí)10ms才能完成的回調(diào)加入poll隊(duì)列并執(zhí)行抖锥,當(dāng)回調(diào)執(zhí)行完成亿眠,poll Queue為空,此時(shí)poll會(huì)去timer階段查看最近有沒有到期的定時(shí)器磅废,發(fā)現(xiàn)存在一個(gè)已經(jīng)超時(shí)將近195ms的定時(shí)器纳像,并執(zhí)行定時(shí)器回調(diào)。在這個(gè)例子中如果不假設(shè)讀取時(shí)間拯勉,定時(shí)器執(zhí)行的時(shí)間間隔大約為200ms竟趾。
注意: 為了防止 poll 階段阻塞事件循環(huán), libuv(一個(gè)實(shí)現(xiàn)了Node.js事件循環(huán)和Node.js平臺(tái)所有異步行為的C語言庫), 有一個(gè)嚴(yán)格的最大限制(這個(gè)值取決于操作系統(tǒng)), 在超過此限制后就會(huì)停止輪詢.
I/O callbacks
此階段執(zhí)行一些系統(tǒng)操作處理 I/O 異常錯(cuò)誤;,如TCP的errors回調(diào)函數(shù)宫峦。
poll
poll 階段主要有兩個(gè)功能:
1.執(zhí)行時(shí)間閾值已過去的定時(shí)器回調(diào)
2.處理poll隊(duì)列中的事件
當(dāng)事件循環(huán)進(jìn)入poll階段并且?當(dāng)前沒有定時(shí)器時(shí)岔帽,以下兩種情況其中一種會(huì)發(fā)生:
如果poll隊(duì)列不是空的,事件循環(huán)會(huì)遍歷隊(duì)列并同步執(zhí)行里面的回調(diào)函數(shù)导绷,直到隊(duì)列為空或者到達(dá)操作系統(tǒng)的限制(操作系統(tǒng)規(guī)定的連續(xù)調(diào)用回調(diào)函數(shù)的數(shù)量的最大值)
如果poll隊(duì)列是空的犀勒,則以下兩種情況其中一種將發(fā)生:
如果存在被?setImmediate()?調(diào)度的回調(diào),事件循環(huán)會(huì)結(jié)束poll階段并進(jìn)入check階段執(zhí)行那些被?setImmediate()?調(diào)度了的回調(diào)妥曲。
如果沒有任何被?setImmediate()?調(diào)度的回調(diào)贾费,事件循環(huán)會(huì)等待回調(diào)函數(shù)被加入隊(duì)列,一旦回調(diào)函數(shù)加入了隊(duì)列檐盟,就立即執(zhí)行它們褂萧。
一旦poll隊(duì)列變?yōu)榭眨录h(huán)就檢查是否已經(jīng)存在超時(shí)的定時(shí)器葵萎,如果存在导犹,事件循環(huán)將繞回到timers階段執(zhí)行這些定時(shí)器回調(diào)唱凯。
check
此階段如果poll階段變?yōu)榭辙D(zhuǎn)(idle)狀態(tài),如果存在被?setImmediate()?調(diào)度的回調(diào)锡足,事件循環(huán)不會(huì)在poll階段阻塞等待相應(yīng)的I/O事件波丰,而直接去check階段執(zhí)行?setImmediate()?函數(shù)。
close callbacks
如果一個(gè)socket或句柄被突然關(guān)閉(例如 socket.destroy()), 'close'事件會(huì)在此階段被觸發(fā). 否則 'close'事件會(huì)通過 process.nextTick() 被觸發(fā).
setImmediate() vs setTimeout()
setImmediate() 被設(shè)計(jì)為: 一旦當(dāng)前的poll階段完成就執(zhí)行回調(diào)
setTimeout() 調(diào)度一個(gè)回調(diào)在時(shí)間閥值之后被執(zhí)行
這兩種定時(shí)器的執(zhí)行順序可能會(huì)變化, 這取決于他們是在哪個(gè)上下文中被調(diào)用的. 如果兩種定時(shí)器都是從主模塊內(nèi)被調(diào)用的, 那么回調(diào)執(zhí)行的時(shí)機(jī)就受進(jìn)程性能的約束(進(jìn)程也會(huì)受到系統(tǒng)中正在運(yùn)行的其他應(yīng)用程序的影響).
setTimeout(functiontimeout(){
console.log('timeout');
},0);
setImmediate(functionimmediate(){
console.log('immediate');
});
但如果把setImmediate和setTimeout放到了I/O周期中舶得,此時(shí)他們的執(zhí)行順序永遠(yuǎn)都是immediate在前,timeout在后爽蝴。
const?fs?=require('fs');
fs.readFile(__filename,()=>{
setTimeout(()=>{
console.log('timeout');
},0);
setImmediate(()=>{
console.log('immediate');
});
});
相比于 setTimeout(), 使用 setImmediate() 的主要優(yōu)點(diǎn)在于: 只要時(shí)在I/O周期內(nèi), 不管已經(jīng)存在多少個(gè)定時(shí)器, setImmediate()設(shè)置的回調(diào)總是在定時(shí)器回調(diào)之前執(zhí)行沐批。
process.nextTick()
在上面我們提到了Next Ticks Queue特殊的隊(duì)列,在這個(gè)隊(duì)列里主要存放process.nextTick這個(gè)異步函數(shù)蝎亚。從技術(shù)上講該階段并不屬于事件循環(huán)的一部分九孩,不管當(dāng)前事件循環(huán)處于哪個(gè)階段,只要當(dāng)前階段操作完畢后進(jìn)入下個(gè)階段前瞬間執(zhí)行process.nextTick()发框。
這樣一來任何時(shí)候在給定階段調(diào)用process.nextTick()時(shí)躺彬,所有傳入process.nextTick()的回調(diào)都會(huì)在事件循環(huán)繼續(xù)之前被執(zhí)行。由于允許開發(fā)者通過遞歸調(diào)用 process.nextTick() 來阻塞I/O操作, 這也使事件循環(huán)無法到達(dá) poll 階段.
利用process.nextTick函數(shù)梅惯,我們可以對內(nèi)部函數(shù)作異步處理可能出現(xiàn)的異常宪拥,porcess.nextTick(callback, ...args)?允許接收多個(gè)參數(shù),callback后面的參數(shù)會(huì)作為callback的實(shí)參傳遞進(jìn)來铣减,這樣就無需嵌套函數(shù)了她君。
function?apiCall(arg,?callback)?{
if(typeofarg?!=='string')
returnprocess.nextTick(callback,
newTypeError('argument?should?be?string'));
callback.call(this,?arg);
};
apiCall(1,(err)=>{
console.log(err);
});
apiCall('node',(err)=>{
console.log(err);
});
setTimeout() setImmediate() process.nextTick()
setTimeout() 在某個(gè)時(shí)間值過后盡快執(zhí)行回調(diào)函數(shù);
process.nextTick() 在當(dāng)前調(diào)用棧結(jié)束后就立即處理葫哗,這時(shí)也必然是“事件循環(huán)繼續(xù)進(jìn)行之前”
setImmediate() 函數(shù)是在poll階段完成后進(jìn)去check階段時(shí)執(zhí)行
優(yōu)先級(jí)順序從高到低: process.nextTick() > setImmediate() > setTimeout()
注:這里只是多數(shù)情況下缔刹,即輪詢階段(I/O 回調(diào)中)。比如之前比較 setImmediate() 和 setTimeout() 的時(shí)候就區(qū)分了所處階段/上下文劣针。
Macrotask Queue和Microtask Queue
macrotask 和 microtask 這兩個(gè)概念, 表示異步任務(wù)的兩種分類校镐。在掛起任務(wù)時(shí),JS 引擎會(huì)將所有任務(wù)按照類別分到這兩個(gè)隊(duì)列中捺典,首先在 macrotask 的隊(duì)列(這個(gè)隊(duì)列也被叫做 task queue)中取出第一個(gè)任務(wù)鸟廓,執(zhí)行完畢后取出 microtask 隊(duì)列中的所有任務(wù)順序執(zhí)行;之后再取 macrotask 任務(wù)辣苏,周而復(fù)始肝箱,直至兩個(gè)隊(duì)列的任務(wù)都取完。
macrotask(宏任務(wù)稀蟋、大任務(wù)):
script(整體代碼)
setTimeout
setInterval
setImmediate
I/O
UI rendering
microtask(微任務(wù)煌张、小任務(wù)):
promise
Object.observe
process.nextTick
MutationObserver
每個(gè)事件循環(huán)只處理一個(gè)macrotask(大任務(wù)) ,但會(huì)處理完所有microtask(小任務(wù))退客。
參考資料
JS運(yùn)行機(jī)制
Node.JS事件循環(huán)
Javascript事件循環(huán)機(jī)制
事件循環(huán)