前言
JavaScript是一門(mén)單線(xiàn)程、非阻塞的腳本語(yǔ)言。
單線(xiàn)程意味著javascript代碼在執(zhí)行的任何時(shí)候,都只有一個(gè)主線(xiàn)程來(lái)處理所有的任務(wù)潜叛。
非阻塞則是當(dāng)代碼需要進(jìn)行一項(xiàng)異步任務(wù)(無(wú)法立刻返回結(jié)果,需要花一定時(shí)間才能返回的任務(wù)壶硅,如I/O事件)的時(shí)候威兜,主線(xiàn)程會(huì)掛起這個(gè)任務(wù),然后在異步任務(wù)返回結(jié)果的時(shí)候再根據(jù)一定規(guī)則去執(zhí)行相應(yīng)的回調(diào)庐椒。
那為什么JavaScript是單線(xiàn)程呢椒舵?是因?yàn)镴avaScript最開(kāi)始的執(zhí)行環(huán)境是在瀏覽器中,而我們需要進(jìn)行各種各樣的dom操作约谈。如果JavaScript是多線(xiàn)程的笔宿,那么當(dāng)兩個(gè)線(xiàn)程同時(shí)對(duì)dom進(jìn)行一項(xiàng)操作犁钟,例如一個(gè)向其添加事件,而另一個(gè)刪除了這個(gè)dom泼橘,那程序就會(huì)出現(xiàn)問(wèn)題涝动。因此,JavaScript只能用一個(gè)主線(xiàn)程來(lái)執(zhí)行代碼炬灭,這樣就保證了程序執(zhí)行的一致性捧存。
但是,單線(xiàn)程雖然保證了程序執(zhí)行順序但是也限制了JavaScript的效率担败,因此H5中新增了web worker技術(shù)。這項(xiàng)技術(shù)可以讓JavaScript多線(xiàn)程運(yùn)行镰官。然而提前,使用web worker技術(shù)開(kāi)的多線(xiàn)程有著諸多限制,例如:所有新線(xiàn)程都受主線(xiàn)程的完全控制泳唠,不能獨(dú)立執(zhí)行狈网。這意味著這些“線(xiàn)程” 實(shí)際上應(yīng)屬于主線(xiàn)程的子線(xiàn)程。另外笨腥,這些子線(xiàn)程并沒(méi)有執(zhí)行I/O操作的權(quán)限拓哺,只能為主線(xiàn)程分擔(dān)一些諸如計(jì)算等任務(wù)。所以嚴(yán)格來(lái)講這些線(xiàn)程并沒(méi)有完整的功能脖母,也因此這項(xiàng)技術(shù)并非改變了JavaScript語(yǔ)言的單線(xiàn)程本質(zhì)士鸥。
而JavaScript的另一個(gè)特點(diǎn)是非阻塞,而非阻塞是因?yàn)槭录h(huán)機(jī)制(event loop)谆级。
本文主要講的是瀏覽器環(huán)境下的事件循環(huán)而非node環(huán)境下的烤礁,兩個(gè)環(huán)境下的事件循環(huán)之間存在差異。
事件循環(huán)
先來(lái)一段代碼:
console.log("start");
setTimeout(function () {
console.log("setTimeout");
}, 1000);
console.log("end");
//運(yùn)行后的結(jié)果如下:
//start
//end
//setTimeout
在控制臺(tái)中運(yùn)行上述代碼肥照,我們可以看出先輸出“start”和"end",然后大約1秒后輸出"setTimeout"脚仔。代碼并有在1s中之后才輸出“end”,而是立即輸出舆绎。這是因?yàn)閟etTimeout方法是一個(gè)異步的函數(shù)鲤脏。也就是說(shuō)代碼中設(shè)置了一個(gè)異步延時(shí)函數(shù)時(shí),代碼并不會(huì)阻塞吕朵,只會(huì)在瀏覽器的事件表中進(jìn)行記錄猎醇,代碼會(huì)繼續(xù)執(zhí)行下去。但延時(shí)的時(shí)間結(jié)束后边锁,事件表會(huì)將setTimeout的回調(diào)函數(shù)添加至事件隊(duì)列(task queue)中姑食,事件隊(duì)列拿到了任何后便將任何壓入到執(zhí)行棧(stack)中,然后執(zhí)行棧執(zhí)行任務(wù)茅坛,輸出"setTImeout"音半。
現(xiàn)在则拷,對(duì)上面的代碼進(jìn)行一些修改:
console.log("start");
setTimeout(function () {
console.log("setTimeout");
}, 0); //這里延時(shí)改為了0
console.log("end");
//運(yùn)行后的結(jié)果如下:
//start
//end
//setTimeout
在代碼中我們將延時(shí)時(shí)間改為0,但是輸出的結(jié)果并沒(méi)有改變曹鸠。這是因?yàn)閟etTimeout的回調(diào)函數(shù)只是被添加到事件隊(duì)列(stack queue)中煌茬,但是不會(huì)立即執(zhí)行。因?yàn)楫?dāng)前的執(zhí)行棧中還有任務(wù)沒(méi)有執(zhí)行結(jié)束彻桃,所以setTimeout任務(wù)還在排隊(duì)坛善,直到”end“輸出后,當(dāng)前的執(zhí)行棧中任務(wù)執(zhí)行完畢邻眷,執(zhí)行棧為空眠屎,這時(shí)候JS引擎便會(huì)檢查事件隊(duì)列,把setTimeout任務(wù)壓入執(zhí)行棧中執(zhí)行肆饶。
根據(jù)上面的代碼我們知道改衩,js引擎遇到一個(gè)異步事件后并不會(huì)一直等待其返回結(jié)果,而是會(huì)將這個(gè)事件掛起驯镊,繼續(xù)執(zhí)行執(zhí)行棧中的其他任務(wù)葫督。當(dāng)一個(gè)異步事件返回結(jié)果后,js會(huì)將這個(gè)事件加入與當(dāng)前執(zhí)行棧不同的另一個(gè)隊(duì)列板惑,我們稱(chēng)之為事件隊(duì)列橄镜。被放入事件隊(duì)列不會(huì)立刻執(zhí)行其回調(diào),而是等待當(dāng)前執(zhí)行棧中的所有任務(wù)都執(zhí)行完畢冯乘,主線(xiàn)程處于閑置狀態(tài)時(shí)洽胶,主線(xiàn)程會(huì)去查找事件隊(duì)列是否有任務(wù)。如果有裆馒,那么主線(xiàn)程會(huì)從中取出排在第一位的事件妖异,并把這個(gè)事件對(duì)應(yīng)的回調(diào)放入執(zhí)行棧中,然后執(zhí)行其中的同步代碼领追,如此反復(fù)他膳,這樣就形成了一個(gè)無(wú)限的循環(huán)。這個(gè)過(guò)程被稱(chēng)為“事件循環(huán)(Event Loop)”绒窑。
上圖中的stack表示我們所說(shuō)的執(zhí)行棧棕孙,Web APIs則是代表一些異步事件,而callback queue即事件隊(duì)列些膨。
進(jìn)階:異步中的Microtasks和Macrotasks
異步任務(wù)分為兩類(lèi):macrotasks(宏任務(wù))和microtasks(微任務(wù)蟀俊,ES2015規(guī)范中稱(chēng)為Job) ,所屬的API如下:
macrotasks(宏任務(wù)):
setTimeout
setInterval
setImmediate
requestAnimationFrame
I/O
UI渲染
microtasks(微任務(wù)):
process.nextTick
promise
Object.observe
MutationObserver
WHATWG規(guī)范:
一個(gè)事件循環(huán)(event loop)會(huì)有一個(gè)或多個(gè)任務(wù)隊(duì)列(task queue)
task queue 就是 macrotask queue
每一個(gè) event loop 都有一個(gè) microtask queue
task queue == macrotask queue != microtask queue
一個(gè)任務(wù) task 可以放入 macrotask queue 也可以放入 microtask queue 中
當(dāng)一個(gè)任務(wù)被放入microtask或者macrotask隊(duì)列后订雾,準(zhǔn)備工作就已經(jīng)結(jié)束肢预,這時(shí)候可以開(kāi)始執(zhí)行任務(wù)了。
根據(jù)上面的描述洼哎,事件循環(huán)的運(yùn)行機(jī)制大概可以分為以下幾個(gè)步驟:
- 檢查事件隊(duì)列是否為空烫映,如果為空沼本,則繼續(xù)檢查;如果不為空锭沟,則執(zhí)行2抽兆;
- 取出macrotask,壓入執(zhí)行棧族淮;
- 執(zhí)行任務(wù)辫红;
- 任務(wù)執(zhí)行完后,檢查microtask隊(duì)列祝辣,如果不為空則執(zhí)行里面的任務(wù)贴妻。如果為空,這執(zhí)行5蝙斜;
- 檢查執(zhí)行棧揍瑟,如果執(zhí)行棧為空,則執(zhí)行1乍炉;如果不為空,則繼續(xù)檢查滤馍;
舉個(gè)例子看是否掌握了:
console.log('start');
setTimeout(function() {
console.log('setTimeout');
new Promise(function (resolve) {
console.log('promise2')
resolve()
}).then(function () {
console.log('then2')
})
},0);
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('then1')
})
console.log('end');
//運(yùn)行后的結(jié)果如下:
//start
//promise1
//end
//then1
//setTimeout
//promise2
//then2
在控制臺(tái)中運(yùn)行上述代碼岛琼,代碼開(kāi)始運(yùn)行時(shí),從macrotask queue中取出任務(wù)執(zhí)行巢株,然后輸出"start"槐瑞,遇到setTimeout,把setTimeout放入macrotask queue中阁苞,繼續(xù)運(yùn)行困檩,遇到實(shí)例化promise輸出"promise1",然后運(yùn)行resolve()那槽,然后遇到promise.then悼沿,放入到microtask queue中,然后輸出"end"骚灸,當(dāng)前macrotask 執(zhí)行完了糟趾,然后取出microtask queue中的任務(wù),輸出"then1"甚牲。當(dāng)前microtask執(zhí)行完后义郑,在macrotask queue取出下一個(gè)macrotask,壓入執(zhí)行棧丈钙,輸出"setTimeout"非驮,然后實(shí)例化promise,輸出"promise2"雏赦,然后遇到promise.then劫笙,放入到microtask queue中芙扎,當(dāng)前macrotask 執(zhí)行完了,然后取出microtask queue中的任務(wù)邀摆,輸出"then2"纵顾。然后繼續(xù)檢查macrotask queue,如果不為空栋盹,則繼續(xù)取出macrotask施逾。為空則繼續(xù)檢查。