摘要:主線程會(huì)暫時(shí)存儲(chǔ)等異步操作,直接向下執(zhí)行,當(dāng)某個(gè)異步事件觸發(fā)時(shí),再通知主線程執(zhí)行相應(yīng)的回調(diào)函數(shù),通過(guò)這種機(jī)制,避免了單線程中異步操作耗時(shí)對(duì)后續(xù)任務(wù)的影響。
背景
在研究js的異步的實(shí)現(xiàn)方式的時(shí)候,發(fā)現(xiàn)了JavaScript 中的 macrotask 和 microtask 的概念。在查閱了一番資料之后,對(duì)其中的執(zhí)行機(jī)制有所了解,下面整理出來(lái),希望可以幫助更多人。
先了解一下js的任務(wù)執(zhí)行機(jī)制首先,javascript是單線程的,所以只能通過(guò)異步解決性能問(wèn)題(否則,如果前面一個(gè)任務(wù)阻塞了,那么后續(xù)的任務(wù)都要等待,這種效果是無(wú)法接受的)。js在執(zhí)行代碼時(shí)存在著兩個(gè)比較重要的東西:執(zhí)行棧和任務(wù)隊(duì)列,這兩個(gè)東西都是用來(lái)存儲(chǔ)任務(wù)的,區(qū)別在于:執(zhí)行棧里面存著的都是同步任務(wù),也就是要按順序執(zhí)行的任務(wù);而任務(wù)隊(duì)列中存著的是一些異步任務(wù),這些異步任務(wù)一定要等到執(zhí)行棧清空后才會(huì)執(zhí)行(這句話很重要)。關(guān)于任務(wù)隊(duì)列,它還分成兩種,一種叫作macrotask queue(姑且這么命名,因?yàn)閲?yán)格來(lái)說(shuō)規(guī)范中只有說(shuō)task,并沒(méi)有提到macrotask這個(gè)概念。這里為了容易區(qū)分,可以理解為macrotask=task!=microtask),另一種叫作microtask queue。如果同時(shí)考慮node環(huán)境和瀏覽器環(huán)境的話,這兩種任務(wù)分別對(duì)應(yīng)以下api:
microtasks:
process.nextTick
promise
Object.observe
MutationObserver
macrotasks:
setTimeout
setInterval
setImmediate
I/O
UI渲染
script標(biāo)簽中的整體代碼
javascript在執(zhí)行時(shí),先從 macrotasks 隊(duì)列開(kāi)始執(zhí)行,取出第一個(gè) macrotask 放入執(zhí)行棧執(zhí)行,在執(zhí)行過(guò)程中,如果遇到 macrotask,則將該 macrotask 放入 macrotask 隊(duì)列,繼續(xù)運(yùn)行執(zhí)行棧中的后續(xù)代碼。如果遇到microtask,那么將該microtask放入microtask隊(duì)列,繼續(xù)向下運(yùn)行執(zhí)行棧中的后續(xù)代碼。當(dāng)執(zhí)行棧中的代碼全部執(zhí)行完成后,從microtasks隊(duì)列中取出所有的microtask放入執(zhí)行棧執(zhí)行。執(zhí)行完畢后,再?gòu)膍acrotasks 隊(duì)列取出下一個(gè)macrotask放入執(zhí)行棧。然后不斷重復(fù)上述流程。這一過(guò)程也被稱作事件循環(huán)(Event Loop)。
javascript就是通過(guò)這種機(jī)制來(lái)實(shí)現(xiàn)異步的。主線程會(huì)暫時(shí)存儲(chǔ)I/O等異步操作,直接向下執(zhí)行,當(dāng)某個(gè)異步事件觸發(fā)時(shí),再通知主線程執(zhí)行相應(yīng)的回調(diào)函數(shù),通過(guò)這種機(jī)制,javascript避免了單線程中異步操作耗時(shí)對(duì)后續(xù)任務(wù)的影響。
根據(jù)圖中描述,一次事件循環(huán)的執(zhí)行步驟如下:
1、從macrotask queue中取出最早的任務(wù)
2、在執(zhí)行棧中執(zhí)行第一步取出的任務(wù)
如果任務(wù)中存在microtask,將其壓入到microtask queue中
如果任務(wù)中存在macrotask,將其壓入到macrotask queue中
直到執(zhí)行完畢
3、執(zhí)行棧設(shè)置為null
4、從macrotask queue中刪除執(zhí)行過(guò)的macrotask
5、取出microtask queue中的全部任務(wù),放入執(zhí)行棧,
如果任務(wù)中存在microtask,將其壓入到microtask queue中
如果任務(wù)中存在macrotask,將其壓入到macrotask queue中
注意:這里產(chǎn)生的microtask(也就是microtask產(chǎn)生的microtask )也會(huì)在這一步驟中執(zhí)行。
直到當(dāng)前microtask queue為空,此步驟結(jié)束。
6、執(zhí)行第一步的操作
我們執(zhí)行如下一段代碼,用上面的思路執(zhí)行,看一下結(jié)果是否和預(yù)期的一致。
console.log("start") const interval = setInterval(() => { console.log("setInterval") }, 0) setTimeout(() => { console.log("setTimeout 1") Promise.resolve() .then(() => { console.log("promise 3") }) .then(() => { console.log("promise 4") }) .then(() => { setTimeout(() => { console.log("setTimeout 2") Promise.resolve() .then(() => { console.log("promise 5") }) .then(() => { console.log("promise 6") }) .then(() => { clearInterval(interval) }) }, 0) }) }, 0) Promise.resolve() .then(() => { console.log("promise 1") }) .then(() => { console.log("promise 2") })
按照上面的思路,我們來(lái)理一下,預(yù)測(cè)一下執(zhí)行結(jié)果,看看實(shí)際效果是否是這樣的。
執(zhí)行流程:
第一輪:
1、首先這一整段js代碼作為一個(gè)macrotask先被執(zhí)行
2、遇到console.log("start"),輸出start
3、遇到setInterval,回調(diào)函數(shù)作為macrotask壓入到macrotask queue中,
此時(shí)macrotask queue:[setInterval]
4、遇到setTimeout,回調(diào)函數(shù)作為macrotask壓入到macrotask queue中,
此時(shí)macrotask queue:[setInterval,setTimeout1]
5、遇到Promise,并且調(diào)用了resolve方法,觸發(fā)了回調(diào),回調(diào)作為microtask壓入到microtask queue中
此時(shí)microtask queue:[promise 1,promise 2]
6、執(zhí)行棧為空,將microtask queue中的任務(wù)放入執(zhí)行棧
7、執(zhí)行microtask queue中Promise的回調(diào)任務(wù),分別打印promise 1,promise 2
8、執(zhí)行棧為空,microtask queue為空,開(kāi)始下一輪事件循環(huán)
目前的console中打印內(nèi)容:
start
promise 1
promise 2
目前macrotask queue:[setInterval,setTimeout1]
第二輪:
1、從macrotask queue中取出最早的任務(wù),這里對(duì)應(yīng)的是第一輪中第3步的回調(diào)函數(shù):console.log("setInterval"),輸出setInterval
2、setInterval的回調(diào)函數(shù)作為macrotask壓入到macrotask queue中
此時(shí)macrotask queue:[setTimeout1,setInterval]
3、執(zhí)行棧為空,microtask queue為空,開(kāi)始下一輪事件循環(huán)
目前的console中打印內(nèi)容:
start
promise 1
promise 2
setInterval
目前macrotask queue:[setTimeout1,setInterval]
第三輪:
1、從macrotask queue中取出最早的任務(wù),目前是setTimeout1的回調(diào),將取出的任務(wù)放入執(zhí)行棧執(zhí)行
2、遇到console.log("setTimeout 1"),輸出setTimeout 1
3、遇到Promise,并且調(diào)用了resolve方法,觸發(fā)回調(diào),回調(diào)作為microtask壓入到microtask queue中
此時(shí)microtask queue:[promise 3,promise 4,() => {setTimeout 2}]
4、執(zhí)行棧為空,將microtask queue中的任務(wù)放入執(zhí)行棧
5、執(zhí)行microtask queue中Promise的回調(diào)任務(wù):
輸出promise 3
輸出promise 4
將setTimeout 2壓入macrotask queue
6、執(zhí)行棧為空,microtask queue為空,開(kāi)始下一輪事件循環(huán)
目前的console中打印內(nèi)容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
目前macrotask queue:[setInterval,setTimeout2]
第四輪:
1、從macrotask queue中取出最早的任務(wù),這里對(duì)應(yīng)的是setInterval,輸出setInterval
2、setInterval的回調(diào)函數(shù)作為macrotask壓入到macrotask queue中
此時(shí)macrotask queue:[setTimeout2,setInterval]
3、執(zhí)行棧為空,microtask queue為空,開(kāi)始下一輪事件循環(huán)
目前的console中打印內(nèi)容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
目前macrotask queue:[setTimeout2,setInterval]
第五輪:
1、從macrotask queue中取出最早的任務(wù),目前是setTimeout2的回調(diào),將取出的任務(wù)放入執(zhí)行棧執(zhí)行
2、遇到console.log("setTimeout 2")輸出setTimeout 2
3、遇到Promise,并且調(diào)用了resolve方法,觸發(fā)回調(diào),回調(diào)作為microtask壓入到microtask queue中
此時(shí)microtask queue:[promise 5,promise 6,() => {clearInterval}]
4、執(zhí)行棧為空,將microtask queue中的任務(wù)放入執(zhí)行棧
5、執(zhí)行microtask queue中Promise的回調(diào)任務(wù):
輸出promise 5
輸出promise 6
clearInterval清空setInterval計(jì)時(shí)器
6、執(zhí)行棧為空,microtask queue為空,macrotask queue為空,任務(wù)結(jié)束。
最終的console中打印內(nèi)容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6
通過(guò)圖片可以看到,結(jié)果跟我們的預(yù)期一致,在promise2的后面作為方法的返回值,多打印了一個(gè)undefined,這個(gè)應(yīng)該好理解的。
這里面有個(gè)小問(wèn)題,就是在不同的環(huán)境下(node/瀏覽器),promise4后面的setInterval表現(xiàn)可能會(huì)有差異,這里可能跟setTimeout和setInterval的最小間隔有關(guān),雖然我們寫(xiě)成0ms,但實(shí)際上這個(gè)最小值是有限制的,現(xiàn)階段不同組織和不同的js引擎實(shí)現(xiàn)機(jī)制存在差異,不過(guò)這個(gè)問(wèn)題不在本次討論范圍之內(nèi)了。如果我們將上述代碼中setInterval的間隔設(shè)置為10,那么整個(gè)執(zhí)行流程將嚴(yán)格符合我們的預(yù)期。
有什么用?后續(xù)我們?cè)诖a中使用Promise,setTimeout時(shí),思路將更加清晰,用起來(lái)更佳得心應(yīng)手。
在閱讀一些源碼時(shí),對(duì)于一些setTimeout相關(guān)的騷操作可以理解的更加深入。
理解javascript中的任務(wù)執(zhí)行流程,加深對(duì)異步流程的理解,少犯錯(cuò)誤。
總結(jié)js事件循環(huán)總是從一個(gè)macrotask開(kāi)始執(zhí)行
一個(gè)事件循環(huán)過(guò)程中,只執(zhí)行一個(gè)macrotask,但是可能執(zhí)行多個(gè)microtask
執(zhí)行棧中的任務(wù)產(chǎn)生的microtask會(huì)在當(dāng)前事件循環(huán)內(nèi)執(zhí)行
執(zhí)行棧中的任務(wù)產(chǎn)生的macrotask要在下一次事件循環(huán)才會(huì)執(zhí)行
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/107861.html
摘要:如果當(dāng)前沒(méi)有事件也沒(méi)有定時(shí)器事件,則返回。相關(guān)資料關(guān)于的架構(gòu)及設(shè)計(jì)思路的事件討論了使用線程池異步運(yùn)行代碼。下一篇初窺事件機(jī)制的實(shí)現(xiàn)二中定時(shí)器的實(shí)現(xiàn) 在瀏覽器中,事件作為一個(gè)極為重要的機(jī)制,給予JavaScript響應(yīng)用戶操作與DOM變化的能力;在Node.js中,事件驅(qū)動(dòng)模型則是其高并發(fā)能力的基礎(chǔ)。 學(xué)習(xí)JavaScript也需要了解它的運(yùn)行平臺(tái),為了更好的理解JavaScript的事...
摘要:階段有兩個(gè)主要功能也會(huì)執(zhí)行時(shí)間定時(shí)器到達(dá)期望時(shí)間的回調(diào)函數(shù)執(zhí)行事件循環(huán)列表里的函數(shù)當(dāng)進(jìn)入階段并且沒(méi)有其余的定時(shí)器,那么如果事件循環(huán)列表不為空,則迭代同步的執(zhí)行隊(duì)列中的函數(shù)。如果沒(méi)有,則等待回調(diào)函數(shù)進(jìn)入隊(duì)列并立即執(zhí)行。 Event Loop 本文以 Node.js 為例,講解 Event Loop 在 Node.js 的實(shí)現(xiàn),原文,JavaScript 中的實(shí)現(xiàn)大同小異。 什么是 Eve...
摘要:新加了一個(gè)微任務(wù)和一個(gè)宏任務(wù)在當(dāng)前執(zhí)行棧的尾部下一次之前觸發(fā)回調(diào)函數(shù)。階段這個(gè)階段主要執(zhí)行一些系統(tǒng)操作帶來(lái)的回調(diào)函數(shù),如錯(cuò)誤,如果嘗試鏈接時(shí)出現(xiàn)錯(cuò)誤,一些會(huì)把這個(gè)錯(cuò)誤報(bào)告給。 JavaScript引擎又稱為JavaScript解釋器,是JavaScript解釋為機(jī)器碼的工具,分別運(yùn)行在瀏覽器和Node中。而根據(jù)上下文的不同,Event loop也有不同的實(shí)現(xiàn):其中Node使用了libu...
摘要:中叫做調(diào)用棧先進(jìn)后出,后進(jìn)先出。如下圖這是典型的內(nèi)存溢出,可能會(huì)出現(xiàn)在某些場(chǎng)景下需要遞歸,但業(yè)務(wù)邏輯中的判斷又沒(méi)能正常計(jì)算進(jìn)入到預(yù)設(shè)情況,于是調(diào)用棧中不斷進(jìn)入,又無(wú)法執(zhí)行完,就造成內(nèi)存溢出了。 本文主要介紹Javascript事件循環(huán)在瀏覽器上的一些特性和應(yīng)用介紹。 Javascript小知識(shí) JavaScript的并發(fā)模型基于事件循環(huán)(Event Loop)。這個(gè)模型與像C或者Jav...
摘要:同時(shí),如果執(zhí)行的過(guò)程中發(fā)現(xiàn)其他函數(shù),繼續(xù)入棧然后執(zhí)行。上面我們討論的其實(shí)都是同步代碼,代碼在運(yùn)行的時(shí)候只用調(diào)用棧解釋就可以了。 序 Event Loop 這個(gè)概念相信大家或多或少都了解過(guò),但是有一次被一個(gè)小伙伴問(wèn)到它具體的原理的時(shí)候,感覺(jué)自己只知道個(gè)大概印象,于是計(jì)劃著寫(xiě)一篇文章,用輸出倒逼輸入,讓自己重新學(xué)習(xí)這個(gè)概念,同時(shí)也能幫助更多的人理解它~ 概念 JavaScript 是一門(mén) ...
摘要:曾經(jīng)的理解首先,是單線程語(yǔ)言,也就意味著同一個(gè)時(shí)間只能做一件事,那么為什么不是多線程呢這樣還能提高效率啊假定同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)節(jié)點(diǎn)上編輯了內(nèi)容,而另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器就很懵逼了,到底以執(zhí)行哪個(gè)操作呢所以,設(shè)計(jì)者把 Event Loop曾經(jīng)的理解 首先,JS是單線程語(yǔ)言,也就意味著同一個(gè)時(shí)間只能做一件事,那么 為什么JavaScript不是多線程呢?這樣還能提...
閱讀 2563·2023-04-26 01:44
閱讀 2571·2021-09-10 10:50
閱讀 1419·2019-08-30 15:56
閱讀 2276·2019-08-30 15:44
閱讀 520·2019-08-29 11:14
閱讀 3425·2019-08-26 11:56
閱讀 3024·2019-08-26 11:52
閱讀 916·2019-08-26 10:27