JavaScript - 事件循環(huán)

前言

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)”绒窑。

image

上圖中的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è)步驟:

  1. 檢查事件隊(duì)列是否為空烫映,如果為空沼本,則繼續(xù)檢查;如果不為空锭沟,則執(zhí)行2抽兆;
  1. 取出macrotask,壓入執(zhí)行棧族淮;
  1. 執(zhí)行任務(wù)辫红;
  1. 任務(wù)執(zhí)行完后,檢查microtask隊(duì)列祝辣,如果不為空則執(zhí)行里面的任務(wù)贴妻。如果為空,這執(zhí)行5蝙斜;
  1. 檢查執(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ù)檢查。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末例获,一起剝皮案震驚了整個(gè)濱河市汉额,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榨汤,老刑警劉巖蠕搜,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異收壕,居然都是意外死亡妓灌,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)蜜宪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)虫埂,“玉大人,你說(shuō)我怎么就攤上這事圃验〉舴” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵澳窑,是天一觀的道長(zhǎng)斧散。 經(jīng)常有香客問(wèn)我,道長(zhǎng)摊聋,這世上最難降的妖魔是什么鸡捐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮麻裁,結(jié)果婚禮上闯参,老公的妹妹穿的比我還像新娘。我一直安慰自己悲立,他們只是感情好鹿寨,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著薪夕,像睡著了一般脚草。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上原献,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天馏慨,我揣著相機(jī)與錄音埂淮,去河邊找鬼。 笑死写隶,一個(gè)胖子當(dāng)著我的面吹牛倔撞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慕趴,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼痪蝇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了冕房?” 一聲冷哼從身側(cè)響起躏啰,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耙册,沒(méi)想到半個(gè)月后给僵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡详拙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年帝际,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饶辙。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹲诀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出畸悬,到底是詐尸還是另有隱情,我是刑警寧澤珊佣,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布蹋宦,位于F島的核電站,受9級(jí)特大地震影響咒锻,放射性物質(zhì)發(fā)生泄漏冷冗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一惑艇、第九天 我趴在偏房一處隱蔽的房頂上張望蒿辙。 院中可真熱鬧,春花似錦滨巴、人聲如沸思灌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泰偿。三九已至,卻和暖如春蜈垮,著一層夾襖步出監(jiān)牢的瞬間耗跛,已是汗流浹背裕照。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留调塌,地道東北人晋南。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像羔砾,于是被迫代替她去往敵國(guó)和親负间。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容