JavaScript事件循環(huán)和任務(wù)隊列

引言

首先需要知道的是JavaScript是門\color{red}{單線程大咱,非阻塞}的語言赌厅。之所以如此設(shè)計咆霜,是因為JavaScript主要應(yīng)用于瀏覽器的互動邓馒,即操作DOM。所以一次只能完成一件任務(wù)蛾坯。如果有多個任務(wù)光酣,就必須排隊,前面一個任務(wù)完成脉课,再執(zhí)行后面一個任務(wù)救军,以此類推。我們假設(shè)JavaScript是多線程的倘零,那么多個線程同時進(jìn)行唱遭,兩個線程同時操作一個DOM,那么以誰的操作為準(zhǔn)呢呈驶?
\color{red}{非阻塞}是當(dāng)代碼需要進(jìn)行一項異步任務(wù)(無法立刻返回結(jié)果拷泽,需要花一定時間才能返回的任務(wù),如I/O事件)的時候,主線程會掛起(pending)這個任務(wù)跌穗,然后在異步任務(wù)返回結(jié)果的時候再根據(jù)一定規(guī)則去執(zhí)行相應(yīng)的回調(diào)订晌。
單線程雖然實現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對單純蚌吸;但是只要有一個任務(wù)耗時很長,后面的任務(wù)都必須排隊等著砌庄,會拖延整個程序的執(zhí)行羹唠。因此為了解決這個問題Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。

同步模式

就是后一個任務(wù)等待前一個任務(wù)結(jié)束娄昆,然后再執(zhí)行佩微,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的萌焰。

異步模式

每一個任務(wù)有一個或多個回調(diào)函數(shù)(callback)哺眯,前一個任務(wù)結(jié)束后,不是執(zhí)行隊列上的后一個任務(wù)扒俯,而是執(zhí)行回調(diào)函數(shù)奶卓;后一個任務(wù)則是不等前一個任務(wù)的回調(diào)函數(shù)的執(zhí)行而執(zhí)行,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的撼玄、異步的夺姑。
"異步模式"非常重要。在瀏覽器端掌猛,耗時很長的操作都應(yīng)該異步執(zhí)行盏浙,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作荔茬。在服務(wù)器端废膘,"異步模式"甚至是唯一的模式,因為執(zhí)行環(huán)境是單線程的慕蔚,如果允許同步執(zhí)行所有http請求丐黄,服務(wù)器性能會急劇下降,很快就會失去響應(yīng)坊萝。

JavaScript為何能執(zhí)行異步任務(wù)

Javascript是單線程的孵稽,但是卻能執(zhí)行異步任務(wù),這主要是因為 JS 中存在\color{red}{事件循環(huán)}(Event Loop)和\color{red}{任務(wù)隊列}(Task Queue)十偶。
示例

setTimeout(function(){
    console.log(2);
},0);
 
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
 
console.log(6);
 
setTimeout(function(){
    console.log(7);
},0);
異步代碼測試結(jié)果.png

事件循環(huán)(Event Loop)

JS 會創(chuàng)建一個類似于 while (true) 的循環(huán)菩鲜,每執(zhí)行一次循環(huán)體的過程稱之為Tick。每次Tick的過程就是查看是否有待處理事件惦积,如果有則取出相關(guān)事件及回調(diào)函數(shù)放入執(zhí)行棧中由主線程執(zhí)行接校。待處理的事件會存儲在一個任務(wù)隊列中,也就是每次Tick會查看任務(wù)隊列中是否有需要執(zhí)行的任務(wù)。

示例

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

事件循環(huán)會按照上圖所示的模式進(jìn)行操作蛛勉,queue.waitForMessage() 會同步地等待消息到達(dá)(如果當(dāng)前沒有任何消息等待被處理)鹿寻。

任務(wù)隊列

和事件循環(huán)聯(lián)系在一起的是任務(wù)隊列,
-所有同步任務(wù)都在主線程上執(zhí)行诽凌,形成一個執(zhí)行棧(execution context stack)毡熏。
-主線程之外,還存在一個”任務(wù)隊列”(task queue)侣诵。只要異步任務(wù)有了運行結(jié)果痢法,就在”任務(wù)隊列”之中放置一個事件。
-一旦”執(zhí)行椂潘常”中的所有同步任務(wù)執(zhí)行完畢财搁,系統(tǒng)就會讀取”任務(wù)隊列”,看看里面有哪些事件躬络。那些對應(yīng)的異步任務(wù)尖奔,于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧穷当,開始執(zhí)行提茁。
-主線程不斷重復(fù)上面的第三步。

異步任務(wù)

異步操作會將相關(guān)回調(diào)添加到任務(wù)隊列中膘滨。而不同的異步操作添加到任務(wù)隊列的時機也不同甘凭,如onclick, setTimeout,ajax 處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的webcore來執(zhí)行的火邓,webcore包含下圖中的3種 webAPI丹弱,分別是DOM Binding、network铲咨、timer模塊躲胳。
-DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發(fā)時纤勒,回調(diào)函數(shù)會立即被-webcore添加到任務(wù)隊列中坯苹。
-network 模塊處理Ajax請求,在網(wǎng)絡(luò)請求返回時摇天,才會將對應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊列中粹湃。
-timer 模塊會對setTimeout等計時器進(jìn)行延時處理,當(dāng)時間到達(dá)的時候泉坐,才會將回調(diào)函數(shù)添加到任務(wù)隊列中为鳄。


webApi.png

事件循環(huán)和任務(wù)隊列之間的關(guān)系

事件循環(huán)規(guī)范.png

規(guī)范中中提到,一個瀏覽器環(huán)境腕让,只能有一個事件循環(huán)孤钦,而一個事件循環(huán)可以多個任務(wù)隊列,每個任務(wù)都有一個任務(wù)源(Task source)。相同任務(wù)源的任務(wù)偏形,只能放到一個任務(wù)隊列中静袖。
不同任務(wù)源的任務(wù),可以放到不同任務(wù)隊列中俊扭。
簡單來說:一個事件循環(huán)可以有多個任務(wù)隊列队橙,隊列之間可有不同的優(yōu)先級,同一隊列中的任務(wù)按先進(jìn)先出的順序執(zhí)行萨惑,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級喘帚,具體實現(xiàn)可能會交叉執(zhí)行。

不同任務(wù)隊列的優(yōu)先級

在異步代碼測試結(jié)果的圖中看到咒钟,代碼的執(zhí)行順序并不是按著,代碼書寫順序依次執(zhí)行的若未,這是因為不同的異步任務(wù)之間也有優(yōu)先級的區(qū)別朱嘴。異步任務(wù)分為兩類,macrotask(宏任務(wù))和 microtask(微任務(wù))

宏任務(wù)(macro task)

script(你的全部JS代碼粗合,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering

微任務(wù)(micro task)

process.nextTick,Promises(這里指瀏覽器原生實現(xiàn)的 Promise), Object.observe, MutationObserver

執(zhí)行順序

瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行萍嬉,會在一個task執(zhí)行結(jié)束后,在下一個 task 執(zhí)行開始前隙疚,對頁面進(jìn)行重新渲染 (task->渲染->task->...)壤追,鼠標(biāo)點擊會觸發(fā)一個事件回調(diào),需要執(zhí)行一個宏任務(wù)供屉,然后解析HTMl行冰。微任務(wù)通常來說就是需要在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù),比如對一系列動作做出反饋伶丐,或或者是需要異步的執(zhí)行任務(wù)而又不需要分配一個新的 task悼做,這樣便可以減小一點性能的開銷。所有微任務(wù)總會在下一個宏任務(wù)之前全部執(zhí)行完畢哗魂。
所以肛走,瀏覽器環(huán)境中,js執(zhí)行任務(wù)的流程是這樣的:
1.第一個事件循環(huán)录别,先執(zhí)行script中的所有同步代碼(即 macrotask 中的第一項任務(wù))
2.再取出 microtask 中的全部任務(wù)執(zhí)行(先清空process.nextTick隊列朽色,再清空promise.then隊列)
3.下一個事件循環(huán),再回到 macrotask 取其中的下一項任務(wù)
4.再重復(fù)2
5.反復(fù)執(zhí)行事件循環(huán)…

一些常見的異步任務(wù)

setTimeout()

將事件插入到了事件隊列组题,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完葫男,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。
當(dāng)主線程時間執(zhí)行過長往踢,無法保證回調(diào)會在事件指定的時間執(zhí)行腾誉。
瀏覽器端每次setTimeout會有4ms的延遲,當(dāng)連續(xù)執(zhí)行多個setTimeout,有可能會阻塞進(jìn)程利职,造成性能問題趣效。

setImmediate()

事件插入到事件隊列尾部,主線程和事件隊列的函數(shù)執(zhí)行完成之后立即執(zhí)行猪贪。和setTimeout(fn,0)的效果差不多跷敬。
服務(wù)端node提供的方法。瀏覽器端最新的api也有類似實現(xiàn):window.setImmediate,但支持的瀏覽器很少热押。

process.nextTick()

插入到事件隊列尾部西傀,但在下次事件隊列之前會執(zhí)行。也就是說桶癣,它指定的任務(wù)總是發(fā)生在所有異步任務(wù)之前拥褂,當(dāng)前主線程的末尾。
大致流程:當(dāng)前”執(zhí)行椦滥”的尾部–>下一次Event Loop(主線程讀取”任務(wù)隊列”)之前–>觸發(fā)process指定的回調(diào)函數(shù)饺鹃。
服務(wù)器端node提供的辦法。用此方法可以用于處于異步延遲的問題间雀。
可以理解為:此次不行悔详,預(yù)約下次優(yōu)先執(zhí)行。

Promise

Promise本身是同步的立即執(zhí)行函數(shù)惹挟, 當(dāng)在 executor 中執(zhí)行 resolve 或者 reject 的時候, 此時是異步操作茄螃, 會先執(zhí)行 then/catch 等,當(dāng)主棧完成后连锯,才會去調(diào)用 resolve/reject 中存放的方法執(zhí)行归苍,打印 p 的時候,是打印的返回結(jié)果萎庭,一個 Promise 實例霜医。

async await

Async/Await就是一個自執(zhí)行的generate函數(shù)。利用generate函數(shù)的特性把異步的代碼寫成“同步”的形式驳规。
async 函數(shù)返回一個 Promise 對象肴敛,當(dāng)函數(shù)執(zhí)行的時候,一旦遇到 await 就會先返回吗购,等到觸發(fā)的異步操作完成医男,再執(zhí)行函數(shù)體內(nèi)后面的語句∧砻悖可以理解為镀梭,是讓出了線程,跳出了 async 函數(shù)體踱启。

參考資料

https://blog.csdn.net/happyqyt/article/details/90644667
https://blog.csdn.net/github_35549695/article/details/82390345
https://www.cnblogs.com/nayek/p/11729923.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop#%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末报账,一起剝皮案震驚了整個濱河市研底,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌透罢,老刑警劉巖榜晦,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異羽圃,居然都是意外死亡乾胶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門朽寞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來识窿,“玉大人,你說我怎么就攤上這事脑融∮髌担” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵肘迎,是天一觀的道長半抱。 經(jīng)常有香客問我,道長膜宋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任炼幔,我火速辦了婚禮秋茫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘乃秀。我一直安慰自己肛著,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布跺讯。 她就那樣靜靜地躺著枢贿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刀脏。 梳的紋絲不亂的頭發(fā)上局荚,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音愈污,去河邊找鬼耀态。 笑死,一個胖子當(dāng)著我的面吹牛暂雹,可吹牛的內(nèi)容都是我干的首装。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杭跪,長吁一口氣:“原來是場噩夢啊……” “哼仙逻!你這毒婦竟也來了驰吓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤系奉,失蹤者是張志新(化名)和其女友劉穎檬贰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喜最,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡偎蘸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瞬内。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迷雪。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖虫蝶,靈堂內(nèi)的尸體忽然破棺而出章咧,到底是詐尸還是另有隱情,我是刑警寧澤能真,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布赁严,位于F島的核電站,受9級特大地震影響粉铐,放射性物質(zhì)發(fā)生泄漏疼约。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一蝙泼、第九天 我趴在偏房一處隱蔽的房頂上張望程剥。 院中可真熱鬧,春花似錦汤踏、人聲如沸织鲸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搂擦。三九已至,卻和暖如春哗脖,著一層夾襖步出監(jiān)牢的瞬間瀑踢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工才避, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丘损,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓工扎,卻偏偏與公主長得像徘钥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肢娘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355