摘要:而這些隊(duì)列由的事件循環(huán)來搞定宏任務(wù)與微任務(wù),在最新標(biāo)準(zhǔn)中,它們被分別稱為與。我們梳理一下事件循環(huán)的執(zhí)行機(jī)制循環(huán)首先從宏任務(wù)開始,遇到,生成執(zhí)行上下文,開始進(jìn)入執(zhí)行棧,可執(zhí)行代碼入棧,依次執(zhí)行代碼,調(diào)用完成出棧。
寫在前面
js是一門單線程的編程語言,也就是說js在處理任務(wù)的時(shí)候,所有任務(wù)只能在一個(gè)線程上排隊(duì)被執(zhí)行,那如果某一個(gè)任務(wù)耗時(shí)比較長(zhǎng)呢?總不能等到它執(zhí)行結(jié)束再去執(zhí)行下一個(gè)。
所以在線程之內(nèi),又被分為了兩個(gè)隊(duì)列:
同步任務(wù)隊(duì)列
異步任務(wù)隊(duì)列
舉個(gè)例子來說:比如你去銀行辦理業(yè)務(wù),都需要領(lǐng)號(hào)排隊(duì)。銀行柜員一個(gè)個(gè)辦理業(yè)務(wù),這時(shí)這個(gè)柜員就相當(dāng)于一個(gè)js線程,客戶排的隊(duì)就相當(dāng)于同步任務(wù)隊(duì)列,每個(gè)人對(duì)于柜員相當(dāng)于一個(gè)個(gè)的任務(wù)。
但這個(gè)時(shí)候,你的電話突然響了,你去接電話接了半小時(shí)。這時(shí)候人家柜員一看你這情況,直接叫了下一個(gè),而你領(lǐng)的號(hào)就作廢了,只能重新零號(hào)排隊(duì)。這時(shí)候你就是被分發(fā)到了異步任務(wù)隊(duì)列。
等你前邊的人都完事了,柜員把你叫過去辦了你的業(yè)務(wù),這時(shí)候就是同步隊(duì)列中的任務(wù)執(zhí)行完了,主線程會(huì)處理異步隊(duì)列中的任務(wù)。
這里說的異步任務(wù),它的意思是包含了獨(dú)立于主執(zhí)行棧之外的宏任務(wù)和微任務(wù)。
先看一個(gè)簡(jiǎn)單的例子,對(duì)這樣的執(zhí)行機(jī)制有個(gè)簡(jiǎn)單的認(rèn)識(shí):
console.log("start") console.log("end")
上邊的執(zhí)行結(jié)果大家肯定都明白,先輸出start,再輸出end,這一段代碼會(huì)進(jìn)入同步隊(duì)列,順序執(zhí)行。
那么我們加點(diǎn)料:
console.log("start") setTimeout(function() { console.log("setTimeout") }, 0) console.log("end")
這樣的情況,函數(shù)調(diào)用棧執(zhí)行到setTimeout時(shí),setTimeout會(huì)在規(guī)定的時(shí)間點(diǎn)將回調(diào)函數(shù)放入異步隊(duì)列,等待同步隊(duì)列的任務(wù)被執(zhí)行完,立即執(zhí)行,所以結(jié)果是:start、end、setTimeout。
但需要注意的一點(diǎn)是,普遍認(rèn)為setTimeout定時(shí)執(zhí)行的認(rèn)知是片面的,因?yàn)榧僭O(shè)setTimeout規(guī)定2秒后執(zhí)行,但同步隊(duì)列中有一個(gè)函數(shù),執(zhí)行花了很長(zhǎng)時(shí)間,甚至花了1秒。那么這時(shí)setTimeout中的回調(diào)也會(huì)等上至少1秒之后,同步任務(wù)都執(zhí)行完了,再去執(zhí)行。這時(shí)候的setTimeout回調(diào)執(zhí)行的時(shí)機(jī)就會(huì)超過2秒,也就是至少3秒。
宏任務(wù)與微任務(wù)宏任務(wù)與微任務(wù)都是獨(dú)立與主執(zhí)行棧之外的另外兩個(gè)隊(duì)列,可以在概念上劃分在異步任務(wù)隊(duì)列里。而這些隊(duì)列由js的事件循環(huán)(EventLoop)來搞定
macro-task(宏任務(wù))與micro-task(微任務(wù)),在最新標(biāo)準(zhǔn)中,它們被分別稱為task與jobs。
由于寫文章時(shí)沒有注意到,實(shí)際上宏任務(wù)與微任務(wù)的概念是不準(zhǔn)確的,但由于文章中涉及多處宏任務(wù)、微任務(wù)的解讀,所以本文暫時(shí)還是用宏任務(wù)、微任務(wù)來分別代指task、jobs。但讀者要明白規(guī)范中沒有宏任務(wù)的概念,只有task與jobs
其中宏任務(wù)(task)包括:
script(整體代碼)
setTimeout, setInterval, setImmediate,
I/O
UI rendering
ajax請(qǐng)求不屬于宏任務(wù),js線程遇到ajax請(qǐng)求,會(huì)將請(qǐng)求交給對(duì)應(yīng)的http線程處理,一旦請(qǐng)求返回結(jié)果,就會(huì)將對(duì)應(yīng)的回調(diào)放入宏任務(wù)隊(duì)列,等請(qǐng)求完成執(zhí)行。
微任務(wù)(jobs)包括:
process.nextTick
Promise
Object.observe(已廢棄)
MutationObserver(html5新特性)
這些我們可以理解為它們?cè)趫?zhí)行上下文中都是可執(zhí)行代碼,會(huì)立即執(zhí)行,只不過會(huì)將各自的回調(diào)函數(shù)放入對(duì)應(yīng)的任務(wù)隊(duì)列中(宏任務(wù)微任務(wù)),也就相當(dāng)于一個(gè)調(diào)度者。
我們梳理一下事件循環(huán)的執(zhí)行機(jī)制:
循環(huán)首先從宏任務(wù)開始,遇到script,生成執(zhí)行上下文,開始進(jìn)入執(zhí)行棧,可執(zhí)行代碼入棧,依次執(zhí)行代碼,調(diào)用完成出棧。
執(zhí)行過程中遇到上邊提到的調(diào)度者,會(huì)同步執(zhí)行調(diào)度者,由調(diào)度者將其負(fù)責(zé)的任務(wù)(回調(diào)函數(shù))放到對(duì)應(yīng)的任務(wù)隊(duì)列中,直到主執(zhí)行棧清空,然后開始執(zhí)行微任務(wù)的任務(wù)隊(duì)列。微任務(wù)也清空后,再次從宏任務(wù)開始,一直循環(huán)這一過程。
上邊說了那么多,還是用一些代碼來驗(yàn)證一下是否是這樣的,先來一個(gè)簡(jiǎn)單一點(diǎn)的。
console.log("start") setTimeout(function() { console.log("timeout") }, 0) new Promise(function(resolve) { console.log("promise") resolve() }).then(function() { console.log("promise resolved") }) console.log("end")
根據(jù)上邊的結(jié)論,分析一下執(zhí)行過程:
建立執(zhí)行上下文,進(jìn)入執(zhí)行棧開始執(zhí)行代碼,打印start
往下執(zhí)行,遇到setTimeout,將回調(diào)函數(shù)放入宏任務(wù)隊(duì)列,等待執(zhí)行
繼續(xù)往下,有個(gè)new Promise,其回調(diào)函數(shù)并不會(huì)被放入其他任務(wù)隊(duì)列,因此會(huì)同步地執(zhí)行,打印promise,但是當(dāng)resolve后,.then會(huì)把其內(nèi)部的回調(diào)函數(shù)放入微任務(wù)隊(duì)列
執(zhí)行到了最底部的代碼,打印出end。這時(shí),主執(zhí)行棧清空了,開始尋找微任務(wù)隊(duì)列里有沒有可執(zhí)行代碼
發(fā)現(xiàn)了微任務(wù)隊(duì)列中有之前放進(jìn)去的代碼,執(zhí)行打印出promise resolved,第一次循環(huán)結(jié)束
再開始第二次循環(huán),從宏任務(wù)開始,檢查宏任務(wù)隊(duì)列是否有可執(zhí)行代碼,發(fā)現(xiàn)有一個(gè),打印timeout
所以,打印順序是:start-->promise-->end-->promise resolved-->timeout
上邊是一個(gè)簡(jiǎn)單示例,比較好理解。那么接下來看一個(gè)稍微復(fù)雜一點(diǎn)的(這里直接用漢字直觀地表明了打印的時(shí)機(jī),避免看起來費(fèi)勁):
console.log("第一次循環(huán)主執(zhí)行棧開始") setTimeout(function() { console.log("第二次循環(huán)開始,宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)執(zhí)行中") new Promise(function(resolve) { console.log("宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)的微任務(wù)繼續(xù)執(zhí)行") resolve() }).then(function() { console.log("第二次循環(huán)的微任務(wù)隊(duì)列的微任務(wù)執(zhí)行") }) }, 0) new Promise(function(resolve) { console.log("第一次循環(huán)主執(zhí)行棧進(jìn)行中...") resolve() }).then(function() { console.log("第一次循環(huán)微任務(wù),第一次循環(huán)結(jié)束") setTimeout(function() { console.log("第二次循環(huán)的宏任務(wù)隊(duì)列的第二個(gè)宏任務(wù)執(zhí)行") }) }) console.log("第一次循環(huán)主執(zhí)行棧完成")
同樣我們分析一下執(zhí)行過程:
第一次循環(huán)
進(jìn)入執(zhí)行棧執(zhí)行代碼,打印第一次循環(huán)主執(zhí)行棧開始
遇到setTimeout,將回調(diào)放入宏任務(wù)隊(duì)列等待執(zhí)行
promise聲明過程是同步的,打印第一次循環(huán)主執(zhí)行棧進(jìn)行中...,resolve后遇到.then,將回調(diào)放入微任務(wù)隊(duì)列
打印第一次循環(huán)主執(zhí)行棧完成
檢查微任務(wù)隊(duì)列是否有可執(zhí)行代碼,有一個(gè)第三步放入的任務(wù),打印第一次循環(huán)微任務(wù),第一次循環(huán)結(jié)束,第一次循環(huán)結(jié)束,同時(shí)遇到setTimeout,將回調(diào)放入宏任務(wù)隊(duì)列
第二次循環(huán)
從宏任務(wù)入手,檢查宏任務(wù)隊(duì)列,發(fā)現(xiàn)有兩個(gè)宏任務(wù),分別是第一次循環(huán)第二步和第一次循環(huán)第五步被放入的任務(wù),先執(zhí)行第一個(gè)宏任務(wù),打印第二次循環(huán)開始,宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)執(zhí)行中
遇到promise聲明語句,打印宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)繼續(xù)執(zhí)行,這時(shí)候又被resolve了,又會(huì)將.then中的回調(diào)放入微任務(wù)隊(duì)列,這是這個(gè)宏任務(wù)隊(duì)列中的第一個(gè)任務(wù)還沒執(zhí)行完
第一個(gè)宏任務(wù)中的同步代碼執(zhí)行完畢,檢查微任務(wù)隊(duì)列,發(fā)現(xiàn)有一段第二步放進(jìn)去的代碼,執(zhí)行打印第二次循環(huán)的微任務(wù)隊(duì)列的微任務(wù)執(zhí)行,此時(shí)第一個(gè)宏任務(wù)執(zhí)行完畢
開始執(zhí)行第二個(gè)宏任務(wù),打印第二次循環(huán)的宏任務(wù)隊(duì)列的第二個(gè)宏任務(wù)執(zhí)行,所有任務(wù)隊(duì)列全部清空,執(zhí)行完畢
所以打印順序?yàn)椋?/p>
第一次循環(huán)主執(zhí)行棧開始
第一次循環(huán)主執(zhí)行棧進(jìn)行中...
第一次循環(huán)主執(zhí)行棧完成
第一次循環(huán)微任務(wù),第一次循環(huán)結(jié)束
第二次循環(huán)開始,宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)執(zhí)行中
第二次循環(huán)的宏任務(wù)隊(duì)列的第一個(gè)宏任務(wù)的微任務(wù)繼續(xù)執(zhí)行
第二次循環(huán)的微任務(wù)隊(duì)列的微任務(wù)執(zhí)行
第二次循環(huán)的宏任務(wù)隊(duì)列的第二個(gè)宏任務(wù)執(zhí)行
看一下gif,事件循環(huán)以肉眼可見的形式呈現(xiàn)出來(兩次循環(huán)之間有微小的時(shí)間間隔)
總結(jié)js的執(zhí)行機(jī)制是面試中??嫉狞c(diǎn),也是非常繞的。但相信完全了解事件循環(huán)機(jī)制,仔細(xì)分析的話,面試遇到這樣的題完全不是問題。我在寫這篇文章的時(shí)候,發(fā)現(xiàn)自己之前理解的很大一部分是錯(cuò)的。如果大家覺得哪里有錯(cuò)誤,還請(qǐng)幫忙指點(diǎn)出來。
歡迎關(guān)注我的公眾號(hào): 一口一個(gè)前端,不定期分享我所理解的前端知識(shí)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/106108.html
摘要:瀏覽器是多進(jìn)程的,而瀏覽器的內(nèi)核渲染進(jìn)程是多線程的。如果已經(jīng)將回調(diào)函數(shù)放進(jìn)任務(wù)隊(duì)列,但是主線程正在執(zhí)行一個(gè)非常耗時(shí)的任務(wù),當(dāng)這個(gè)任務(wù)執(zhí)行完畢后,主線程去任務(wù)隊(duì)列中取任務(wù),這個(gè)時(shí)候,就會(huì)出現(xiàn)連續(xù)執(zhí)行的情況,也就是說相當(dāng)于失效了。 前言 ??在刷筆試題的時(shí)候,經(jīng)常會(huì)碰到setTimeout的問題,只知道這個(gè)是設(shè)置定時(shí)器;但是考察的重點(diǎn)一般是在一個(gè)方法中包含了定時(shí)器,定時(shí)器中的打印和方法中打...
摘要:圖片轉(zhuǎn)引自的演講和兩個(gè)定時(shí)器中回調(diào)的執(zhí)行邏輯便是典型的機(jī)制。異步編程關(guān)于異步編程我的理解是,在執(zhí)行環(huán)境所提供的異步機(jī)制之上,在應(yīng)用編碼層面上實(shí)現(xiàn)整體流程控制的異步風(fēng)格。 問題背景 在一次開發(fā)任務(wù)中,需要實(shí)現(xiàn)如下一個(gè)餅狀圖動(dòng)畫,基于canvas進(jìn)行繪圖,但由于對(duì)于JS運(yùn)行環(huán)境中異步機(jī)制的不了解,所以遇到了一個(gè)棘手的問題,始終無法解決,之后在與同事交流之后才恍然大悟。問題的根節(jié)在于經(jīng)典的J...
摘要:事件觸發(fā)線程主要負(fù)責(zé)將準(zhǔn)備好的事件交給引擎線程執(zhí)行。進(jìn)程瀏覽器渲染進(jìn)程瀏覽器內(nèi)核,主要負(fù)責(zé)頁面的渲染執(zhí)行以及事件的循環(huán)。第二輪循環(huán)結(jié)束。 將自己讀到的比較好的文章分享出來,大家互相學(xué)習(xí),各位大佬有好的文章也可以留個(gè)鏈接互相學(xué)習(xí),萬分感謝! 線程與進(jìn)程 關(guān)于線程與進(jìn)程的關(guān)系可以用下面的圖進(jìn)行說明: showImg(https://segmentfault.com/img/bVbjSZt?...
摘要:事件觸發(fā)線程主要負(fù)責(zé)將準(zhǔn)備好的事件交給引擎線程執(zhí)行。進(jìn)程瀏覽器渲染進(jìn)程瀏覽器內(nèi)核,主要負(fù)責(zé)頁面的渲染執(zhí)行以及事件的循環(huán)。第二輪循環(huán)結(jié)束。 將自己讀到的比較好的文章分享出來,大家互相學(xué)習(xí),各位大佬有好的文章也可以留個(gè)鏈接互相學(xué)習(xí),萬分感謝! 線程與進(jìn)程 關(guān)于線程與進(jìn)程的關(guān)系可以用下面的圖進(jìn)行說明: showImg(https://segmentfault.com/img/bVbjSZt?...
閱讀 1261·2021-09-04 16:41
閱讀 2427·2021-09-02 10:18
閱讀 927·2019-08-29 16:40
閱讀 2624·2019-08-29 16:14
閱讀 918·2019-08-26 13:41
閱讀 1311·2019-08-26 12:24
閱讀 742·2019-08-26 10:24
閱讀 2882·2019-08-23 17:54