javascript的運(yùn)行機(jī)制锻弓,Event Loop
單線程
javascript為什么是單線程語(yǔ)言砾赔,原因在于如果是多線程,當(dāng)一個(gè)線程對(duì)DOM節(jié)點(diǎn)做添加內(nèi)容操作的時(shí)候青灼,另一個(gè)線程要?jiǎng)h除這個(gè)DOM節(jié)點(diǎn)暴心,這個(gè)時(shí)候,瀏覽器應(yīng)該怎么選擇杂拨,這就造成了混亂专普,為了解決這類(lèi)問(wèn)題,在一開(kāi)始的時(shí)候弹沽,javascript就采用單線程模式檀夹。
在后面H5出的web worker標(biāo)準(zhǔn)的時(shí)候筋粗,看似是多線程,其實(shí)是在一個(gè)主線程來(lái)控制其他線程击胜,而且不能操作DOM亏狰,所以本質(zhì)還是單線程
任務(wù)隊(duì)列
任務(wù)可以分為兩種役纹,一種為同步偶摔,另一種為異步(具有回調(diào)函數(shù))。如下圖:
所有的同步任務(wù)都在主線程上執(zhí)行促脉,形成一個(gè)執(zhí)行棧 stack辰斋。當(dāng)所有同步任務(wù)執(zhí)行完畢后,它會(huì)去執(zhí)行microtask queue中的異步任務(wù)(nextTick瘸味,Promise)宫仗,將他們?nèi)繄?zhí)行。主線程之外還有一個(gè)任務(wù)隊(duì)列task queue旁仿,當(dāng)有異步任務(wù)(DOM藕夫,AJAX,setTimeout枯冈,setImmediate)有結(jié)果的時(shí)候毅贮,就在任務(wù)隊(duì)列里放一個(gè)事件,一旦執(zhí)行棧和microtask queue任務(wù)執(zhí)行完畢尘奏,系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列滩褥,將取出排在最前面的事件加入執(zhí)行棧執(zhí)行,這種機(jī)制就是任務(wù)隊(duì)列炫加。
Event Loop
主線程在任務(wù)隊(duì)列中讀取事件瑰煎,這個(gè)過(guò)程是循環(huán)不斷地,所以這種運(yùn)行機(jī)制叫做Event Loop(事件循環(huán))
nextTick俗孝、setImmediate酒甸、setTimeout
nextTick是在執(zhí)行棧同步代碼結(jié)束之后,下一次Event Loop(任務(wù)隊(duì)列)執(zhí)行之前赋铝。當(dāng)所有同步任務(wù)執(zhí)行完烘挫,會(huì)在queue中執(zhí)行nextTick,無(wú)論nextTick有多少層回調(diào)柬甥,都會(huì)執(zhí)行完畢后再去任務(wù)隊(duì)列饮六,所以會(huì)造成一直停留在當(dāng)前執(zhí)行棧,無(wú)法執(zhí)行任務(wù)隊(duì)列苛蒲,請(qǐng)看下面代碼
process.nextTick(function () {
console.log('nextTick1');
process.nextTick(function (){console.log('nextTick2')});
});
setTimeout(function timeout() {
console.log('setTimeout');
}, 0)
執(zhí)行完畢后輸出nextTick1卤橄、nextTick2、setTimeout臂外,原因是nextTick是在當(dāng)前執(zhí)行棧末尾執(zhí)行窟扑,而setTimeout是在下次任務(wù)隊(duì)列在執(zhí)行
setImmediate方法是在Event Loop(任務(wù)隊(duì)列)末尾喇颁,也就是下一次Event Loop時(shí)執(zhí)行。
setTimeout方法是按照?qǐng)?zhí)行時(shí)間嚎货,放入任務(wù)隊(duì)列橘霎,有時(shí)快與setImmediate有時(shí)慢。請(qǐng)看以下代碼
setImmediate(function () {
console.log('setImmediate1');
setImmediate(function (){console.log('setImmediate2')});
});
setTimeout(function timeout() {
console.log('setTimeout');
}, 0);
這段代碼執(zhí)行完可能是setImmediate1殖属、setTimeout姐叁、setImmediate2,也可能是setTimeout洗显、setImmediate1外潜、setImmediate2,原因是setTimeout和setImmediate1都是在下次Event Loop中觸發(fā)挠唆,所以先后不確定处窥,但是setImmediate2肯定是最后,因?yàn)樗窃趕etImmediate1任務(wù)隊(duì)列之后玄组,也就是下下次Event Loop執(zhí)行
Node.js的Event Loop
Node.js也是單線程的Event Loop但是和瀏覽器有些區(qū)別滔驾,如圖所示,
1.先通過(guò)Chrom V8引擎解析Javascript腳本
2.解析完畢后調(diào)用Node API
3.LIBUV庫(kù)負(fù)責(zé)Node API的執(zhí)行俄讹,將不同任務(wù)分配給不同的線程哆致,形成一個(gè)Event Loop(任務(wù)隊(duì)列)
4.最后Chrom V8引擎將結(jié)果返回給用戶
Node.js Event Loop原理
node.js的特點(diǎn)是事件驅(qū)動(dòng),非阻塞單線程颅悉。當(dāng)應(yīng)用程序需要I/O操作的時(shí)候沽瞭,線程并不會(huì)阻塞,而是把I/O操作交給底層庫(kù)(LIBUV)剩瓶。此時(shí)node線程會(huì)去處理其他任務(wù)驹溃,當(dāng)?shù)讓訋?kù)處理完I/O操作后,會(huì)將主動(dòng)權(quán)交還給Node線程延曙,所以Event Loop的用處是調(diào)度線程豌鹤,例如:當(dāng)?shù)讓訋?kù)處理I/O操作后調(diào)度Node線程處理后續(xù)工作,所以雖然node是單線程枝缔,但是底層庫(kù)處理操作依然是多線程
Node Event Loop的事件處理機(jī)制
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
上面處理階段都是按照先進(jìn)先出的規(guī)則執(zhí)行回調(diào)函數(shù)布疙,按順序執(zhí)行,直到隊(duì)列為空或是該階段執(zhí)行的回調(diào)函數(shù)達(dá)到該階段所允許一次執(zhí)行回調(diào)函數(shù)的最大限制后愿卸,才會(huì)將操作權(quán)移交給下一階段灵临。
- timers: 用來(lái)檢查setTimeout()和setInterval()定時(shí)器是否到期,如果到期則執(zhí)行它趴荸,否則下一階段
- I/O callbacks: 用來(lái)處理timers階段儒溉、setImmediate、和TCP他們的異撤⒍郏回調(diào)函數(shù)或者error
- idle, prepare: nodejs內(nèi)部函數(shù)調(diào)用顿涣,在循環(huán)被I/O阻塞之前prepare回調(diào)就會(huì)立即調(diào)用
- poll: 用來(lái)監(jiān)聽(tīng)fd的事件的波闹,比如socket的可讀,可寫(xiě)涛碑,文件的可讀可等等
- check: setImmediate()函數(shù)只會(huì)在這個(gè)階段執(zhí)行
- close callbacks: 執(zhí)行一些諸如關(guān)閉事件的回調(diào)函數(shù)精堕,如socket.on('close', ...)
具體分析,看下圖:
1.當(dāng)setTimeout時(shí)間最小蒲障,讀取文件不存在的時(shí)候
eventloop-setTimeout0-unfile.png
如圖所示歹篓,分別是nextTick、readFile晌涕、setTimeout滋捶、setImmediate痛悯,然而現(xiàn)在并沒(méi)有1.txt和2.txt文件余黎,輸出結(jié)果是next Tick、setTimeout载萌、readFile惧财、setImmediate,在event loop中先判斷的是timeers,最先出書(shū)next Tick因?yàn)閜rocess.nextTick的實(shí)現(xiàn)是基于v8 MicroTask(是在當(dāng)前js call stack 中沒(méi)有可執(zhí)行代碼才會(huì)執(zhí)行的隊(duì)列,低于js call stack 代碼,但高于事件循環(huán)聋亡,不屬于Event Loop犀斋,上面javascript的Event Loop介紹過(guò)了,所以最先輸出萎攒。然后開(kāi)始走Event Loop,第一階段是timers,判斷setTimeout到期仰迁,所以輸出setTimeout,進(jìn)入下一階段顽分,poll將I/O操作權(quán)交出徐许,新線程操作,但是并沒(méi)有相關(guān)讀取文件卒蘸,所以直接返回回調(diào)函數(shù)雌隅,所以處處readFile,最后到check階段缸沃,輸出setImmediate
2.當(dāng)setTimeout時(shí)間最小恰起,讀取文件存在的時(shí)候
eventloop-setTimeout0-file.png
如圖所示,分別是nextTick趾牧、setTimeout检盼、setImmediate、readFile武氓,這次readFile在最后面梯皿,是因?yàn)槲募嬖诔鹣洌瑘?zhí)行到poll階段的時(shí)候,執(zhí)行I/O操作东羹,node線程開(kāi)始執(zhí)行check階段剂桥,當(dāng)交出的I/O操作結(jié)束后,返回給Event Loop所以再執(zhí)行readFile的回調(diào)函數(shù)属提,所以他在最后面
3.當(dāng)setTimeout時(shí)間為100毫秒权逗,讀取文件不存在的時(shí)候
eventloop-setTimeout100-unfile.png
如圖所示,分別是nextTick冤议、readFile斟薇、setImmediate、setTimeout恕酸,它和1不同的地方是setTimeout排在最后了堪滨,這是因?yàn)樵趫?zhí)行timers的時(shí)候,setTimeout沒(méi)有到期蕊温,所以直接執(zhí)行下一階段袱箱,當(dāng)執(zhí)行完poll的時(shí)候,會(huì)去執(zhí)行查看定時(shí)器有沒(méi)有到期义矛,如果沒(méi)有下一次Event Loop再次查看发笔,知道定時(shí)器到期,所以他在最后面
4.當(dāng)setTimeout時(shí)間為100毫秒凉翻,讀取文件存在的時(shí)候
eventloop-setTimeout100-file.png
如圖所示了讨,分別是nextTick、setImmediate制轰、readFile前计、setTimeout,它和2的區(qū)別是setTimeout在最后艇挨,原因和3一樣残炮。
總結(jié)一下
1.javascript和node.js都是單線程,但是node底層是多線程操作
2.Event Loop —— 任務(wù)隊(duì)列
3.當(dāng)同時(shí)設(shè)置nextTick, setImmediate, setTimeout時(shí)一定是nextTick先執(zhí)行缩滨,nextTick不屬于Event LOop势就,它屬于v8的micro tasks,并且會(huì)阻塞Event Loop
4.setImmediate脉漏,setTimeout屬于Event Loop但是苞冯,直接階段不同