原文鏈接
一篇文章搞懂瀏覽器Js事件循環(huán)機(jī)制(推薦閱讀!)
前言
在初次入門學(xué)習(xí)和使用 JavaScript 的過程中,相信遇到過許多程序執(zhí)行順序及結(jié)果與預(yù)期不一致的問題稀轨,在查閱資料的過程中了解到原來是程序的執(zhí)行有同步與異步之分;與此同時(shí)也會看到許多有關(guān)概念岸军,例如回調(diào)函數(shù)奋刽、執(zhí)行棧、任務(wù)隊(duì)列艰赞、事件循環(huán)機(jī)制(Event Loop)佣谐、宏任務(wù)、微任務(wù)方妖、Promise(ES6)等等狭魂。此時(shí)對于一個(gè)剛?cè)腴T不久的小白來說,要理解消化這些概念真的不容易。對于入門不久的我來說也一樣雌澄,所以寫一篇博客記錄一下斋泄,有關(guān) JavaScript 的運(yùn)行機(jī)制,以及上述的這些概念為什么會出現(xiàn)镐牺,又解決了什么問題炫掐。
一、JavaScript 是單線程
我們知道多線程是可以并行執(zhí)行程序的睬涧,能提高程序運(yùn)行效率募胃。但是 JS 是一門單線程語言,同一時(shí)間內(nèi)做一件事畦浓。
最初作為服務(wù)于瀏覽器的腳本語言痹束,很多時(shí)候都是在與用戶交互,這個(gè)過程涉及了許多 DOM 的操作讶请,倘若使用多線程参袱,那么就容易出現(xiàn)幾個(gè)線程同時(shí)操作一個(gè) DOM 的問題,那么瀏覽器此時(shí)要以哪一個(gè)線程為主呢秽梅?這樣一來無疑增加了復(fù)雜性抹蚀,所以 JS 成為了單線程。雖然說多線程處理起來也很高效企垦,但對于當(dāng)時(shí)直接服務(wù)于瀏覽器用戶的 JS 來說环壤,盡可能避免過度復(fù)雜,能更簡單的處理相對好點(diǎn)吧钞诡。
二郑现、異步任務(wù)及其回調(diào)函數(shù)
雖然單線程降低了復(fù)雜性,但是也有了新的問題荧降。單線程是順序執(zhí)行程序接箫,每一個(gè)任務(wù)要等待上一個(gè)任務(wù)執(zhí)行完畢才執(zhí)行,如果遇到執(zhí)行時(shí)間太長或者出現(xiàn)了別的問題朵诫,那么就會一直卡在那辛友,導(dǎo)致整個(gè)程序無法順利執(zhí)行完畢。為了解決問題剪返,語言設(shè)計(jì)者希望在程序執(zhí)行時(shí)废累,將一些耗時(shí)、有延遲的任務(wù)先掛起脱盲,讓能快速執(zhí)行完畢的任務(wù)先執(zhí)行邑滨;按照這樣的方式執(zhí)行完整個(gè)程序后,在返回去執(zhí)行那些被掛起的任務(wù)钱反。因此有了同步任務(wù)與異步任務(wù)之分掖看;在執(zhí)行過程中匣距,當(dāng)前執(zhí)行程序的線程稱為主線程,同步任務(wù)直接在主線程立即執(zhí)行哎壳,而那些異步任務(wù)毅待,先給它掛在一邊放著,等到主線程執(zhí)行完了所有同步任務(wù)耳峦,再回來讀取掛在一旁的異步任務(wù)恩静,并且執(zhí)行他們。
(1) 任務(wù)隊(duì)列
任務(wù)隊(duì)列是一系列事件組成的一個(gè)隊(duì)列蹲坷,也就是上面說到的異步任務(wù)掛起的地方驶乾。程序執(zhí)行時(shí)會將定義的異步任務(wù)送入任務(wù)隊(duì)列,或者用戶點(diǎn)擊鼠標(biāo)觸發(fā)的異步任務(wù)送入隊(duì)列循签。等待主線程來執(zhí)行它們级乐。例如常見的各種事件(鼠標(biāo)點(diǎn)擊、鍵盤敲擊县匠、滾動等等)风科、又或者是 Ajax 那樣等待響應(yīng)的異步任務(wù)。
實(shí)際上乞旦,任務(wù)隊(duì)列不止一種贼穆,因?yàn)樘幚淼漠惒饺蝿?wù)種類可能不同
(2) 回調(diào)函數(shù) (callback)
回調(diào)函數(shù)往往就是異步任務(wù)所定義的代碼。主線程執(zhí)行完同步任務(wù)兰粉,就會回來開始讀取任務(wù)隊(duì)列中的異步任務(wù)并執(zhí)行這些代碼故痊,同時(shí)也稱為回調(diào)函數(shù)。
(3) 宏任務(wù)和微任務(wù)
異步任務(wù)又可以看為兩種玖姑,通常由宿主環(huán)境(瀏覽器愕秫、node)提供的為宏任務(wù),由語言標(biāo)準(zhǔn)提供的為微任務(wù)焰络。 JavaScript 可能會在不同的宿主環(huán)境下運(yùn)行戴甩,所以宏任務(wù)來自于宿主環(huán)境,而微任務(wù)作為語言標(biāo)準(zhǔn)闪彼,在任何環(huán)境下都可以使用甜孤。
常見宏任務(wù)
- setTimeout
- setInterval
- setImmediate (僅 node 提供)
- requestAnimationFrame (僅瀏覽器提供)
- 各種交互 (鼠標(biāo)點(diǎn)擊、滾動等等)
- I/O
常見微任務(wù)
- Promise.then catch finally
- MutationObserver (僅瀏覽器提供)
- process.nextTick (僅 node 提供)
三备蚓、事件循環(huán)機(jī)制 (Event Loop)
主線程執(zhí)行程序時(shí)會將定義的異步任務(wù)放入任務(wù)隊(duì)列中课蔬,宏任務(wù)會放在宏任務(wù)隊(duì)列,微任務(wù)放在微任務(wù)隊(duì)列郊尝,當(dāng)觸發(fā) UI 事件時(shí),也會把相應(yīng)任務(wù)放入隊(duì)列战惊。為了確保事件處理正常進(jìn)行流昏,主線程不阻塞扎即。所以有了解決方案 Event Loop,事件循環(huán)線程是獨(dú)立于主線程的况凉,并且一直存在直到整個(gè)腳本環(huán)境被關(guān)閉谚鄙。無論是主線程執(zhí)行時(shí)添加的異步任務(wù),還是 UI 交互觸發(fā)后添加的異步任務(wù)刁绒,事件循環(huán)機(jī)制都會按一定規(guī)則循環(huán)讀取并且執(zhí)行闷营。
那么該循環(huán)機(jī)制如何運(yùn)行呢?
(1) 打開某個(gè)宿主環(huán)境時(shí)知市,主線程執(zhí)行同步任務(wù)的所有代碼傻盟,形成一個(gè)執(zhí)行棧;把遇到的異步任務(wù)放入相應(yīng)的隊(duì)列里嫂丙;同時(shí)一個(gè)獨(dú)立于主線程的事件循環(huán)線程也被創(chuàng)建并一直存在娘赴。
(2) 當(dāng)主線程執(zhí)行完同步任務(wù),會將該執(zhí)行過程中添加的微任務(wù)全部執(zhí)行完跟啤,之后由事件循環(huán)機(jī)制協(xié)調(diào)诽表。
(3) 事件循環(huán)讀取當(dāng)前宏任務(wù)隊(duì)列的一個(gè)宏任務(wù),并放入執(zhí)行棧中執(zhí)行
(4) 在執(zhí)行過程中遇到宏任務(wù)和微任務(wù)隅肥,按照相同的方式放入相應(yīng)隊(duì)列
(5) 該宏任務(wù)執(zhí)行完畢后立即執(zhí)行此次宏任務(wù)中所添加的所有微任務(wù)
(6) 回到第 (3) 步開始重復(fù)后面步驟竿奏。
說那么多,看個(gè)例子
console.log('1-1');
Promise.resolve().then(() => console.log('微任務(wù) 1-1'));
new Promise((resolve) => {
console.log('1-2');
resolve();
}).then(() => {
console.log('微任務(wù) 1-2')
});
setTimeout(() => console.log('宏任務(wù) 1-1'), 100);
console.log('1-3');
//1-1
//1-2
//1-3
//微任務(wù) 1-1
//微任務(wù) 1-2
//宏任務(wù) 1-1
主線程開始執(zhí)行腥放,形成一個(gè)執(zhí)行棧
碰到第一個(gè) console.log('1-1')泛啸,并打印 -> 1-1
碰到第一個(gè) Promise,已為成功狀態(tài)捉片,將其 then() 加到微任務(wù)中
碰到第二個(gè) Promise平痰,先執(zhí)行其中的 console.log('1-2'),打印 -> 1-2伍纫,并將其 then() 放入微任務(wù)隊(duì)列
碰到第一個(gè)宏任務(wù)宗雇,放入宏任務(wù)隊(duì)列
碰到 console.log('1-3'),打印 -> 1-3
主線程執(zhí)行完所有同步任務(wù)莹规,開始執(zhí)行本次添加的所有微任務(wù)
讀取微任務(wù)隊(duì)列
遇到先進(jìn)去的第一個(gè) then() 赔蒲,打印 -> 微任務(wù) 1-1
遇到后進(jìn)去的 then() 打印 -> 微任務(wù) 1-2
本次主線程任務(wù)完成,下面由事件循環(huán)機(jī)制來協(xié)調(diào)良漱。開始讀取宏任務(wù)隊(duì)列
遇到第一個(gè)放入的宏任務(wù) setTimeout()舞虱,將其丟到執(zhí)行棧延時(shí) 100ms 執(zhí)行,打印 -> 宏任務(wù) 1-1
第一次宏任務(wù)執(zhí)行完畢母市,讀取微任務(wù)隊(duì)列矾兜,發(fā)現(xiàn)沒有微任務(wù)。進(jìn)入第二次循環(huán)
讀取宏任務(wù)隊(duì)列患久,發(fā)現(xiàn)沒有宏任務(wù)椅寺。JS 執(zhí)行棧開始摸魚...
到這里其實(shí)會發(fā)現(xiàn)浑槽,微任務(wù)都會緊跟在當(dāng)前執(zhí)行棧執(zhí)行同步任務(wù)后執(zhí)行,而存好的宏任務(wù)被放在下次執(zhí)行返帕,好似重新開始一樣桐玻。
按個(gè)人總結(jié)來就是(不一定對),主線程的執(zhí)行棧是專門用來執(zhí)行代碼的荆萤;當(dāng)事件循環(huán)線程讀取到一個(gè)宏任務(wù)時(shí)镊靴,將其放入執(zhí)行棧執(zhí)行,主線程會執(zhí)行其中定義的同步任務(wù)链韭,將遇到的宏任務(wù)和微任務(wù)存起來偏竟,在本次同步任務(wù)執(zhí)行完之后立即執(zhí)行微任務(wù)。而此次存好的宏任務(wù)又會按照相同的方式在下一次循環(huán)中進(jìn)行梧油。因?yàn)槭录h(huán)機(jī)制一次循環(huán)只讀取執(zhí)行一個(gè)宏任務(wù)苫耸。
由此看來其實(shí)整個(gè)程序也可以看成是一個(gè)宏任務(wù),而首次添加的宏任務(wù)和微任務(wù)是按照上面的方式一層層刨開儡陨,按照一次執(zhí)行一個(gè)宏任務(wù)和里面所有微任務(wù)的規(guī)則進(jìn)行
- 再看個(gè)例子說明宏任務(wù)是一次循環(huán)讀取一次褪子,并且會執(zhí)行宏任務(wù)下所有微任務(wù)
console.log('開始執(zhí)行主線程');
console.log('0-1');
Promise.resolve().then(() => console.log('微任務(wù) 0-1\n-----'));
setTimeout(() => {//宏任務(wù) 1
console.log('第一個(gè)宏任務(wù)');
console.log('宏任務(wù) 1-1');
Promise.resolve().then(() => console.log('微任務(wù) 1-1'));
Promise.resolve().then(() => console.log('微任務(wù) 1-2\n-----'));
setTimeout(() => {//宏任務(wù)3
console.log('第三個(gè)宏任務(wù)');
console.log('宏任務(wù) 3-1')
Promise.resolve().then(() => console.log('微任務(wù) 3-1\n-----'))
},10);
},100);
setTimeout(() => {//宏任務(wù)2
console.log('第二個(gè)宏任務(wù)');
console.log('宏任務(wù)2-1');
Promise.resolve().then(() => console.log('微任務(wù) 2-1\n-----'));
},100);
console.log('0-2');
***************************
執(zhí)行結(jié)果
開始執(zhí)行主線程
0-1
0-2
微任務(wù) 0-1
-----
第一個(gè)宏任務(wù)
宏任務(wù) 1-1
微任務(wù) 1-1
微任務(wù) 1-2
-----
第二個(gè)宏任務(wù)
宏任務(wù) 2-1
微任務(wù) 2-1
-----
第三個(gè)宏任務(wù)
宏任務(wù) 3-1
微任務(wù) 3-1
-----
- 開始執(zhí)行主線程后,將 微任務(wù) 0-1 骗村、 宏任務(wù)1 嫌褪、 宏任務(wù)2 存入隊(duì)列,并先打印其同步任務(wù)代碼胚股,又打印微任務(wù)代碼
- 開始第一次事件循環(huán)笼痛,讀取宏任務(wù)1(第一個(gè)定時(shí)),將 微任務(wù) 1-1 琅拌、微任務(wù) 1-2缨伊、和宏任務(wù)3 存入隊(duì)列。打印方式如上一條进宝。
- 開始第二次事件循環(huán)刻坊,讀取宏任務(wù)2(第二個(gè)定時(shí)),將 微任務(wù) 2-1 存入隊(duì)列党晋,打印方式如上谭胚。
- 開始第三次事件循環(huán),讀取宏任務(wù)隊(duì)列中最后一個(gè)進(jìn)去的宏任務(wù)3(宏任務(wù)1中定義的定時(shí)器)未玻,將 微任務(wù) 3-1 存入隊(duì)列灾而,打印方式如上。
大概流程圖
提示扳剿,雖然說是一次循環(huán)只讀取一個(gè)宏任務(wù)旁趟,但是他沒說要等當(dāng)前宏任務(wù)執(zhí)行完才進(jìn)行下一次循環(huán)哦!庇绽!轻庆,事件循環(huán)讀取到隊(duì)列中的任務(wù)并且讓它開始執(zhí)行后癣猾,就可以開始下次循環(huán)敛劝,不需要等待
- 下面改動的例子余爆,留給自己做練習(xí)吧
console.log(1);
Promise.resolve().then(() => console.log(2));
setTimeout(() => {
console.log(3);
Promise.resolve().then(() => console.log(4));
setTimeout(() => {
console.log(5);
},10);
},200);
setTimeout(() => {
console.log(6);
Promise.resolve().then(() => console.log(7));
setTimeout(() => {
console.log(8)
}, 300);
},100);
console.log(9);
自己在紙上寫了一下,將代碼在瀏覽器上運(yùn)行之后對比夸盟,發(fā)現(xiàn)完全正確蛾方。你也可以自己寫一下哦。
2020/9/22 更新
有一種情況上陕,那就是 then() 之后接著 then() 桩砰,那么此時(shí)的順序呢?
console.log('1-1');
Promise.resolve().then(() => console.log('微任務(wù) 1-1')).then(() => console.log('微任務(wù) 1-3'));
new Promise((resolve) => {
console.log('1-2');
resolve();
}).then(() => {
console.log('微任務(wù) 1-2')
}).then(() => console.log('微任務(wù) 1-4'));
setTimeout(() => console.log('宏任務(wù) 1-1'), 100);
//1-1
//1-2
//1-3
//微任務(wù) 1-1
//微任務(wù) 1-2
//微任務(wù) 1-3
//微任務(wù) 1-4
//宏任務(wù) 1-1
可以看到他的運(yùn)行順序释簿,說明在 then() 執(zhí)行之后亚隅,如果后面還接著 then() 那么按照同樣的方式添加到微任務(wù)隊(duì)列,等到之前添加的第一層 then() 都執(zhí)行完后庶溶,在到微任務(wù)隊(duì)列里面讀取后面添加的 then()煮纵,運(yùn)行方式如上。并且只有當(dāng)微任務(wù)隊(duì)列為空時(shí)偏螺,事件循環(huán)機(jī)制才會進(jìn)行到下一輪并讀取新的宏任務(wù)行疏。
參考鏈接
阮一峰的網(wǎng)絡(luò)日志
JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop
知乎作者:tigerHee
js中的宏任務(wù)與微任務(wù)
博客園作者:daisy,gogogo
JavaScipt 中的事件循環(huán) event loop,以及微任務(wù)和宏任務(wù)的概念
國外作者寫的一篇文章
Tasks, microtasks, queues and schedules