手把手帶你徹底掌握,任務(wù)隊(duì)列、事件循環(huán)、宏任務(wù)、微任務(wù)
調(diào)用棧 Call Stack
正式闡述任務(wù)隊(duì)列與事件循環(huán),大概了解一下 JavaScript
是如何運(yùn)行的:
在 JavaScript
運(yùn)行的時(shí)候,主線程會(huì)形成一個(gè)棧,這個(gè)棧主要是解釋器用來最終函數(shù)執(zhí)行流的一種機(jī)制。通常這個(gè)棧被稱為調(diào)用棧 Call Stack
,或者執(zhí)行棧( Execution Context Stack
)。
調(diào)用棧,顧名思義是具有LIFO(后進(jìn)先出,Last in First Out)的結(jié)構(gòu)。調(diào)用棧內(nèi)存放的是代碼執(zhí)行期間的所有執(zhí)行上下文。
-
每調(diào)用一個(gè)函數(shù),解釋器就會(huì)把該函數(shù)的執(zhí)行上下文添加到調(diào)用棧并開始執(zhí)行;
-
正在調(diào)用棧中執(zhí)行的函數(shù),如果還調(diào)用了其他函數(shù),那么新函數(shù)也會(huì)被添加到調(diào)用棧,并立即執(zhí)行;
-
當(dāng)前函數(shù)執(zhí)行完畢后,解釋器會(huì)將其執(zhí)行上下文清除調(diào)用棧,繼續(xù)執(zhí)行剩余執(zhí)行上下文中的剩余代碼;
-
但分配的調(diào)用棧空間被占滿,會(huì)引發(fā)”堆棧溢出“的報(bào)錯(cuò)。
調(diào)用棧 Call Stack參考文章:
1、 juejin.cn/post/696902… [1]
2、 blog.csdn.net/ch834301/ar… [2]
1. 為何需要有任務(wù)隊(duì)列與循環(huán)事件
1、JavaScript 是 單線程的 :一次只能運(yùn)行一個(gè)任務(wù)。通常,這沒什么大不了的,但是現(xiàn)在想象你正在運(yùn)行一個(gè)耗時(shí) 30 秒的任務(wù),比如請(qǐng)求數(shù)據(jù)、定時(shí)器、讀取文件等等。在此任務(wù)中,我們等待 30 秒才能進(jìn)行其他任何操作(默認(rèn)情況下,JavaScript 在瀏覽器的主線程上運(yùn)行,因此整個(gè)用戶界面都停滯了),后面的語(yǔ)句就得一直等著前面的語(yǔ)句執(zhí)行結(jié)束后才會(huì)開始執(zhí)行 。
都到 2021 年了,沒有人想停留在一個(gè)速度慢,交互反應(yīng)遲鈍的網(wǎng)站。
2、瀏覽器每個(gè)渲染進(jìn)程都有一個(gè)主線程,并且主線程非常繁忙,既要處理 DOM,又 要計(jì)算樣式,還要處理布局,同時(shí)還需要處理 JavaScript 任務(wù)以及各種輸入事件。要讓這 么多不同類型的任務(wù)在主線程中有條不紊地執(zhí)行,這就需要一個(gè)系統(tǒng)來統(tǒng)籌調(diào)度這些任務(wù), 這個(gè)統(tǒng)籌調(diào)度系統(tǒng)就是我們今天要講的消息隊(duì)列和事件循環(huán)系統(tǒng)。
(不清楚瀏覽器渲染時(shí)候,進(jìn)程線程如何運(yùn)行的同學(xué),等我下一篇文章總結(jié)一下,后期我會(huì)加入文章鏈接)
3、要想在線程運(yùn)行過程中,能接收并執(zhí)行新的任務(wù),就需要采用事件循環(huán)機(jī)制。
4、能夠接收其他線程發(fā)送的消息呢,一個(gè)通用模式是使用消息隊(duì)列。
同步任務(wù)和異步任務(wù)
因此, JavaScript
將所有執(zhí)行任務(wù)分為了同步任務(wù)和異步任務(wù)。
其實(shí)我們每個(gè)任務(wù)都是在做兩件事情,就是 發(fā)起調(diào)用 和 得到結(jié)果 。
而同步任務(wù)和異步任務(wù)最主要的差別就是,同步任務(wù)發(fā)起調(diào)用后,很快就可以得到結(jié)果,而異步任務(wù)是無法立即得到結(jié)果,比如請(qǐng)求接口,每個(gè)接口都會(huì)有一定的響應(yīng)時(shí)間,根據(jù)網(wǎng)速、服務(wù)器等等因素決定,再比如定時(shí)器,它需要固定時(shí)間后才會(huì)返回結(jié)果。
因此,對(duì)于同步任務(wù)和異步任務(wù)的執(zhí)行機(jī)制也不同。
同步任務(wù)的執(zhí)行,其實(shí)就是跟前面那個(gè)案例一樣,按照代碼順序和調(diào)用順序,支持進(jìn)入調(diào)用棧中并執(zhí)行,執(zhí)行結(jié)束后就移除調(diào)用棧。
而異步任務(wù)的執(zhí)行,首先它依舊會(huì)進(jìn)入調(diào)用棧中,然后發(fā)起調(diào)用,然后解釋器會(huì)將其 響應(yīng)回調(diào)任務(wù) 放入一個(gè) 任務(wù)隊(duì)列 ,緊接著調(diào)用棧會(huì)將這個(gè)任務(wù)移除。當(dāng)主線程清空后,即所有同步任務(wù)結(jié)束后,解釋器會(huì)讀取任務(wù)隊(duì)列,并依次將 已完成的異步任務(wù) 加入調(diào)用棧中并執(zhí)行。
這里有個(gè)重點(diǎn),就是異步任務(wù)不是直接進(jìn)入任務(wù)隊(duì)列的,等執(zhí)行到異步函數(shù)(任務(wù))的回調(diào)函數(shù)推入到任務(wù)隊(duì)列中。
img-blog.csdnimg.cn/20210629235… [3]
任務(wù)入隊(duì)
這里還有一個(gè)知識(shí)點(diǎn),就是關(guān)于任務(wù)入隊(duì)。
任務(wù)進(jìn)入任務(wù)隊(duì)列,其實(shí)會(huì)利用到瀏覽器的其他線程。雖然說 JavaScript
是單線程語(yǔ)言,但是瀏覽器不是單線程的。而不同的線程就會(huì)對(duì)不同的事件進(jìn)行處理,當(dāng)對(duì)應(yīng)事件可以執(zhí)行的時(shí)候,對(duì)應(yīng)線程就會(huì)將其放入任務(wù)隊(duì)列。
-
js引擎線程:用于解釋執(zhí)行js代碼、用戶輸入、網(wǎng)絡(luò)請(qǐng)求等;
-
GUI渲染線程:繪制用戶界面,與JS主線程互斥(因?yàn)閖s可以操作DOM,進(jìn)而會(huì)影響到GUI的渲染結(jié)果);
-
http異步網(wǎng)絡(luò)請(qǐng)求線程:處理用戶的get、post等請(qǐng)求,等返回結(jié)果后將回調(diào)函數(shù)推入到任務(wù)隊(duì)列;
- 定時(shí)觸發(fā)器線程 :
setInterval
、setTimeout
等待時(shí)間結(jié)束后,會(huì)把執(zhí)行函數(shù)推入任務(wù)隊(duì)列中; - 瀏覽器事件處理線程 :將
click
、mouse
等UI交互事件發(fā)生后,將要執(zhí)行的回調(diào)函數(shù)放入到事件隊(duì)列中。
2. 任務(wù)隊(duì)列與循環(huán)事件到底是個(gè)啥
1、消息(任務(wù))隊(duì)列
消息隊(duì)列是一種數(shù)據(jù)結(jié)構(gòu),可以存放要執(zhí)行的任務(wù)。它符合隊(duì)列“先進(jìn)先出”的特點(diǎn),也就是說要添加任務(wù)的話,添加到隊(duì)列的尾部;要取出任務(wù)的話,從隊(duì)列頭部去取。
在任務(wù)隊(duì)列中,其實(shí)還分為 宏任務(wù)隊(duì)列(Task Queue)**和**微任務(wù)隊(duì)列(Microtask Queue) ,對(duì)應(yīng)的里面存放的就是 宏任務(wù) 和 微任務(wù) 。
首先,宏任務(wù)和微任務(wù)都是異步任務(wù)。
補(bǔ)充個(gè)知識(shí)點(diǎn):1、常見的宏任務(wù):script(整體代碼) setTimeout setInterval I/O UI交互事件 postMessage MessageChannel setImmediate(Node.js 環(huán)境) 2、常見的微任務(wù):Promise.then Object.observe MutaionObserver process.nextTick(Node.js 環(huán)境)
2、事件循環(huán)系統(tǒng)
事件循環(huán)系統(tǒng)就是在監(jiān)聽并執(zhí)行消息隊(duì)列中的任務(wù)
3. 任務(wù)隊(duì)列與循環(huán)事件具體如何使用
事件循環(huán) Event Loop
其實(shí)宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列的執(zhí)行,就是事件循環(huán)的一部分了,所以放在這里一起說。
事件循環(huán)的具體流程如下:
-
從宏任務(wù)隊(duì)列中,按照 入隊(duì)順序 ,找到第一個(gè)執(zhí)行的宏任務(wù),放入調(diào)用棧,開始執(zhí)行;
-
執(zhí)行完 該宏任務(wù) 下所有同步任務(wù)后,即調(diào)用棧清空后,該宏任務(wù)被推出宏任務(wù)隊(duì)列,然后微任務(wù)隊(duì)列開始按照入隊(duì)順序,依次執(zhí)行其中的微任務(wù), 直至微任務(wù)隊(duì)列清空為止 ;
-
當(dāng)微任務(wù)隊(duì)列清空后,一個(gè)事件循環(huán)結(jié)束;
-
接著從宏任務(wù)隊(duì)列中,找到下一個(gè)執(zhí)行的宏任務(wù),開始第二個(gè)事件循環(huán),直至宏任務(wù)隊(duì)列清空為止。
這里有幾個(gè)重點(diǎn):
- 當(dāng)我們第一次執(zhí)行的時(shí)候,解釋器會(huì)將整體代碼
script
放入宏任務(wù)隊(duì)列中,因此事件循環(huán)是從第一個(gè)宏任務(wù)開始的; -
如果在執(zhí)行微任務(wù)的過程中,產(chǎn)生新的微任務(wù)添加到微任務(wù)隊(duì)列中,也需要一起清空;微任務(wù)隊(duì)列沒清空之前,是不會(huì)執(zhí)行下一個(gè)宏任務(wù)的。
4. 詳解宏任務(wù)(如: setTimeout() )
為了協(xié)調(diào)這些任務(wù)有條不紊地在主線程上執(zhí)行,頁(yè)面進(jìn)程引入了消息隊(duì)列和事件循環(huán)機(jī)制, 渲染進(jìn)程內(nèi)部會(huì)維護(hù)多個(gè)消息隊(duì)列,比如(延遲執(zhí)行隊(duì)列和普通的消息隊(duì)列)。然后主線程采用 一個(gè) for 循環(huán),不斷地從這些任務(wù)隊(duì)列中取出任務(wù)并執(zhí)行任務(wù)。我們把這些消息隊(duì)列中的任 務(wù)稱為宏任務(wù)。
- 當(dāng)我們第一次執(zhí)行的時(shí)候,解釋器會(huì)將整體代碼
script
放入宏任務(wù)隊(duì)列中,因此事件循環(huán)是從第一個(gè)宏任務(wù)開始的; -
如果在執(zhí)行微任務(wù)的過程中,產(chǎn)生新的微任務(wù)添加到微任務(wù)隊(duì)列中,也需要一起清空;微任務(wù)隊(duì)列沒清空之前,是不會(huì)執(zhí)行下一個(gè)宏任務(wù)的。
參考文章:
1、 juejin.cn/post/696902… [5]
5. 詳解微任務(wù)(如:promise、MutationObserver)
微任務(wù)就是一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束 之前。
我們知道當(dāng) JavaScript 執(zhí)行一段腳本的時(shí)候,V8 會(huì)為其創(chuàng)建一個(gè)全局執(zhí)行上下文,在創(chuàng)建 全局執(zhí)行上下文的同時(shí),V8 引擎也會(huì)在內(nèi)部創(chuàng)建一個(gè)微任務(wù)隊(duì)列。顧名思義,這個(gè)微任務(wù) 隊(duì)列就是用來存放微任務(wù)的,因?yàn)樵诋?dāng)前宏任務(wù)執(zhí)行的過程中,有時(shí)候會(huì)產(chǎn)生多個(gè)微任務(wù), 這時(shí)候就需要使用這個(gè)微任務(wù)隊(duì)列來保存這些微任務(wù)了。不過這個(gè)微任務(wù)隊(duì)列是給 V8 引擎 內(nèi)部使用的,所以你是無法通過 JavaScript 直接訪問的。
也就是說每個(gè)宏任務(wù)都關(guān)聯(lián)了一個(gè)微任務(wù)隊(duì)列。那么接下來,我們就需要分析兩個(gè)重要的時(shí) 間點(diǎn)——微任務(wù)產(chǎn)生的時(shí)機(jī)和執(zhí)行微任務(wù)隊(duì)列的時(shí)機(jī)。我們先來看看微任務(wù)是怎么產(chǎn)生的?在現(xiàn)代瀏覽器里面,產(chǎn)生微任務(wù)有兩種方式。第一種方式是使用 MutationObserver 監(jiān)控某個(gè) DOM 節(jié)點(diǎn),然后再通過 JavaScript 來修 改這個(gè)節(jié)點(diǎn),或者為這個(gè)節(jié)點(diǎn)添加、刪除部分子節(jié)點(diǎn),當(dāng) DOM 節(jié)點(diǎn)發(fā)生變化時(shí),就會(huì)產(chǎn) 生 DOM 變化記錄的微任務(wù)。第二種方式是使用 Promise,當(dāng)調(diào)用 Promise.resolve() 或者 Promise.reject() 的時(shí)候,也 會(huì)產(chǎn)生微任務(wù)。
好了,現(xiàn)在微任務(wù)隊(duì)列中有了微任務(wù)了,那接下來就要看看微任務(wù)隊(duì)列是何時(shí)被執(zhí)行的。通常情況下,在當(dāng)前宏任務(wù)中的 JavaScript 快執(zhí)行完成時(shí),也就在 JavaScript 引擎準(zhǔn)備退 出全局執(zhí)行上下文并清空調(diào)用棧的時(shí)候,JavaScript 引擎會(huì)檢查全局執(zhí)行上下文中的微任 務(wù)隊(duì)列,然后按照順序執(zhí)行隊(duì)列中的微任務(wù)。WHATWG 把執(zhí)行微任務(wù)的時(shí)間點(diǎn)稱為檢查 點(diǎn)。當(dāng)然除了在退出全局執(zhí)行上下文式這個(gè)檢查點(diǎn)之外,還有其他的檢查點(diǎn),不過不是太重 要,這里就不做介紹了。如果在執(zhí)行微任務(wù)的過程中,產(chǎn)生了新的微任務(wù),同樣會(huì)將該微任務(wù)添加到微任務(wù)隊(duì)列中, V8 引擎一直循環(huán)執(zhí)行微任務(wù)隊(duì)列中的任務(wù),直到隊(duì)列為空才算執(zhí)行結(jié)束。也就是說在執(zhí)行 微任務(wù)過程中產(chǎn)生的新的微任務(wù)并不會(huì)推遲到下個(gè)宏任務(wù)中執(zhí)行,而是在當(dāng)前的宏任務(wù)中繼 續(xù)執(zhí)行。
Demo案例:
該示意圖是在執(zhí)行一個(gè) ParseHTML 的宏任務(wù),在執(zhí)行過程中,遇到了 JavaScript 腳本, 那么就暫停解析流程,進(jìn)入到 JavaScript 的執(zhí)行環(huán)境。從圖中可以看到,全局上下文中包 含了微任務(wù)列表。在 JavaScript 腳本的后續(xù)執(zhí)行過程中,分別通過 Promise 和 removeChild 創(chuàng)建了兩個(gè)微 任務(wù),并被添加到微任務(wù)列表中。接著 JavaScript 執(zhí)行結(jié)束,準(zhǔn)備退出全局執(zhí)行上下文, 這時(shí)候就到了檢查點(diǎn)了,JavaScript 引擎會(huì)檢查微任務(wù)列表,發(fā)現(xiàn)微任務(wù)列表中有微任 務(wù),那么接下來,依次執(zhí)行這兩個(gè)微任務(wù)。等微任務(wù)隊(duì)列清空之后,就退出全局執(zhí)行上下 文。
注意點(diǎn):
微任務(wù)和宏任務(wù)是綁定的,每個(gè)宏任務(wù)在執(zhí)行時(shí),會(huì)創(chuàng)建自己的微任務(wù)隊(duì)列。微任務(wù)的執(zhí)行時(shí)長(zhǎng)會(huì)影響到當(dāng)前宏任務(wù)的時(shí)長(zhǎng)。比如一個(gè)宏任務(wù)在執(zhí)行過程中,產(chǎn)生了 100 個(gè)微任務(wù),執(zhí)行每個(gè)微任務(wù)的時(shí)間是 10 毫秒,那么執(zhí)行這 100 個(gè)微任務(wù)的時(shí)間就 是 1000 毫秒,也可以說這 100 個(gè)微任務(wù)讓宏任務(wù)的執(zhí)行時(shí)間延長(zhǎng)了 1000 毫秒。所以 你在寫代碼的時(shí)候一定要注意控制微任務(wù)的執(zhí)行時(shí)長(zhǎng)。在一個(gè)宏任務(wù)中,分別創(chuàng)建一個(gè)用于回調(diào)的宏任務(wù)和微任務(wù),無論什么情況下,微任務(wù)都 早于宏任務(wù)執(zhí)行。
參考文章:
1、 time.geekbang.org/column/arti… [6]
6. 詳解async、await
async 會(huì)將其后的函數(shù)(函數(shù)表達(dá)式或 Lambda)的返回值封裝成一個(gè) Promise 對(duì)象,而 await 會(huì)等待這個(gè) Promise 完成,并將其 resolve 的結(jié)果返回出來。
ES7 引入了一個(gè)新的在
JavaScript 中添加異步行為的方式并且使
promise 用起來更加簡(jiǎn)單!隨著
async 和
await 關(guān)鍵字的引入,我們能夠創(chuàng)建一個(gè)隱式的返回一個(gè)
promise 的
async` 函數(shù)。但是,我們?cè)撛趺醋瞿兀?/p>
之前,我們看到不管是通過輸入 new Promise(() => {})
, Promise.resolve
或 Promise.reject
,我們都可以顯式的使用 Promise
對(duì)象創(chuàng)建 promise
。
我們現(xiàn)在能夠創(chuàng)建隱式地返回一個(gè)對(duì)象的異步函數(shù),而不是顯式地使用 Promise
對(duì)象!這意味著我們不再需要寫任何 Promise
對(duì)象了。
盡管 async
函數(shù)隱式的返回 promise
是一個(gè)非常棒的事實(shí),但是在使用 await
關(guān)鍵字的時(shí)候才能看到 async
函數(shù)的真正力量。當(dāng)我們等待 await
后的值返回一個(gè) resolved
的 promise
時(shí),通過 await
關(guān)鍵字,我們可以暫停異步函數(shù)。如果我們想要得到這個(gè) resolved
的 promise
的值,就像我們之前用 then
回調(diào)那樣,我們可以為被 await
的 promise
的值賦值為變量!
具體案例請(qǐng)參考下面五星文章哦,
五星提醒必看文章:
1、驚艷!可視化的 js:動(dòng)態(tài)圖演示 Promises & Async/Await 的過程!
mp.weixin.qq.com/s\?\_\_biz=MzA… [7]
2、驚艷!可視化的 js:動(dòng)態(tài)圖演示 - 事件循環(huán) Event Loop
blog.csdn.net/ch834301/ar… [8]
-
原文地址: dev.to/lydiahallie… [9]
-
原文作者:Lydia Hallie
一個(gè)js函數(shù)簡(jiǎn)單執(zhí)行流程(簡(jiǎn)單總結(jié)):
一個(gè)js函數(shù)簡(jiǎn)單執(zhí)行流程:
先執(zhí)行該函數(shù)里面的同步方法,全部執(zhí)行完同步任務(wù)以后, 比如:var num=10 , console.log('timeout') 這種步驟
再執(zhí)行微任務(wù)的回調(diào)函數(shù),全部執(zhí)行完微任務(wù)的回調(diào)函數(shù), 比如:Promise.resolve(5).then(res => res_2).then(res => res_2)
最后執(zhí)行該函數(shù)里面的宏任務(wù)的回調(diào)函數(shù)。比如:setTimeout(() => { console.log('timeout') },0)
(前提:不同任務(wù)存在的情況下,沒有就不執(zhí)行)---