摘要:所以本來快輪到你來辦理業(yè)務(wù),會因為老大爺臨時添加的理財業(yè)務(wù)而往后推。在執(zhí)行完同步代碼與微任務(wù)以后,這時繼續(xù)向后查找有木有宏任務(wù)。所以輸出了第二次,等到這兩次都執(zhí)行完畢后才會去檢查有沒有微任務(wù)有沒有宏任務(wù)。
首先,JavaScript是一個單線程的腳本語言。
所以就是說在一行代碼執(zhí)行的過程中,必然不會存在同時執(zhí)行的另一行代碼,就像使用alert()以后進行瘋狂console.log,如果沒有關(guān)閉彈框,控制臺是不會顯示出一條log信息的。
亦或者有些代碼執(zhí)行了大量計算,比方說在前端暴力破解密碼之類的鬼操作,這就會導(dǎo)致后續(xù)代碼一直在等待,頁面處于假死狀態(tài),因為前邊的代碼并沒有執(zhí)行完。
所以如果全部代碼都是同步執(zhí)行的,這會引發(fā)很嚴(yán)重的問題,比方說我們要從遠(yuǎn)端獲取一些數(shù)據(jù),難道要一直循環(huán)代碼去判斷是否拿到了返回結(jié)果么?_就像去飯店點餐,肯定不能說點完了以后就去后廚催著人炒菜的,會被揍的。_
于是就有了異步事件的概念,注冊一個回調(diào)函數(shù),比如說發(fā)一個網(wǎng)絡(luò)請求,我們告訴主程序等到接收到數(shù)據(jù)后通知我,然后我們就可以去做其他的事情了。
然后在異步完成后,會通知到我們,但是此時可能程序正在做其他的事情,所以即使異步完成了也需要在一旁等待,等到程序空閑下來才有時間去看哪些異步已經(jīng)完成了,可以去執(zhí)行。
比如說打了個車,如果司機先到了,但是你手頭還有點兒事情要處理,這時司機是不可能自己先開著車走的,一定要等到你處理完事情上了車才能走。
這個就像去銀行辦業(yè)務(wù)一樣,先要取號進行排號。
一般上邊都會印著類似:“您的號碼為XX,前邊還有XX人?!敝惖淖謽?。
因為柜員同時職能處理一個來辦理業(yè)務(wù)的客戶,這時每一個來辦理業(yè)務(wù)的人就可以認(rèn)為是銀行柜員的一個宏任務(wù)來存在的,當(dāng)柜員處理完當(dāng)前客戶的問題以后,選擇接待下一位,廣播報號,也就是下一個宏任務(wù)的開始。
所以多個宏任務(wù)合在一起就可以認(rèn)為說有一個任務(wù)隊列在這,里邊是當(dāng)前銀行中所有排號的客戶。
任務(wù)隊列中的都是已經(jīng)完成的異步操作,而不是說注冊一個異步任務(wù)就會被放在這個任務(wù)隊列中,就像在銀行中排號,如果叫到你的時候你不在,那么你當(dāng)前的號牌就作廢了,柜員會選擇直接跳過進行下一個客戶的業(yè)務(wù)處理,等你回來以后還需要重新取號
而且一個宏任務(wù)在執(zhí)行的過程中,是可以添加一些微任務(wù)的,就像在柜臺辦理業(yè)務(wù),你前邊的一位老大爺可能在存款,在存款這個業(yè)務(wù)辦理完以后,柜員會問老大爺還有沒有其他需要辦理的業(yè)務(wù),這時老大爺想了一下:“最近P2P爆雷有點兒多,是不是要選擇穩(wěn)一些的理財呢”,然后告訴柜員說,要辦一些理財?shù)臉I(yè)務(wù),這時候柜員肯定不能告訴老大爺說:“您再上后邊取個號去,重新排隊”。
所以本來快輪到你來辦理業(yè)務(wù),會因為老大爺臨時添加的“理財業(yè)務(wù)”而往后推。
也許老大爺在辦完理財以后還想 再辦一個信用卡?或者 再買點兒紀(jì)念幣?
無論是什么需求,只要是柜員能夠幫她辦理的,都會在處理你的業(yè)務(wù)之前來做這些事情,這些都可以認(rèn)為是微任務(wù)。
這就說明:你大爺永遠(yuǎn)是你大爺
在當(dāng)前的微任務(wù)沒有執(zhí)行完成時,是不會執(zhí)行下一個宏任務(wù)的。
所以就有了那個經(jīng)常在面試題、各種博客中的代碼片段:
setTimeout(_ => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then(_ => { console.log(3) }) console.log(2)
setTimeout就是作為宏任務(wù)來存在的,而Promise.then則是具有代表性的微任務(wù),上述代碼的執(zhí)行順序就是按照序號來輸出的。
所有會進入的異步都是指的事件回調(diào)中的那部分代碼
也就是說new Promise在實例化的過程中所執(zhí)行的代碼都是同步進行的,而then中注冊的回調(diào)才是異步執(zhí)行的。
在同步代碼執(zhí)行完成后才回去檢查是否有異步任務(wù)完成,并執(zhí)行對應(yīng)的回調(diào),而微任務(wù)又會在宏任務(wù)之前執(zhí)行。
所以就得到了上述的輸出結(jié)論1、2、3、4。
+部分表示同步執(zhí)行的代碼
+setTimeout(_ => { - console.log(4) +}) +new Promise(resolve => { + resolve() + console.log(1) +}).then(_ => { - console.log(3) +}) +console.log(2)
本來setTimeout已經(jīng)先設(shè)置了定時器(相當(dāng)于取號),然后在當(dāng)前進程中又添加了一些Promise的處理(臨時添加業(yè)務(wù))。
所以進階的,即便我們繼續(xù)在Promise中實例化Promise,其輸出依然會早于setTimeout的宏任務(wù):
setTimeout(_ => console.log(4)) new Promise(resolve => { resolve() console.log(1) }).then(_ => { console.log(3) Promise.resolve().then(_ => { console.log("before timeout") }).then(_ => { Promise.resolve().then(_ => { console.log("also before timeout") }) }) }) console.log(2)
當(dāng)然了,實際情況下很少會有簡單的這么調(diào)用Promise的,一般都會在里邊有其他的異步操作,比如fetch、fs.readFile之類的操作。
而這些其實就相當(dāng)于注冊了一個宏任務(wù),而非是微任務(wù)。
P.S. 在Promise/A+的規(guī)范中,Promise的實現(xiàn)可以是微任務(wù),也可以是宏任務(wù),但是普遍的共識表示(至少Chrome是這么做的),Promise應(yīng)該是屬于微任務(wù)陣營的
所以,明白哪些操作是宏任務(wù)、哪些是微任務(wù)就變得很關(guān)鍵,這是目前業(yè)界比較流行的說法:
宏任務(wù)# | 瀏覽器 | Node |
---|---|---|
I/O | ? | ? |
setTimeout | ? | ? |
setInterval | ? | ? |
setImmediate | ? | ? |
requestAnimationFrame | ? | ? |
有些地方會列出來UI Rendering,說這個也是宏任務(wù),可是在讀了HTML規(guī)范文檔以后,發(fā)現(xiàn)這很顯然是和微任務(wù)平行的一個操作步驟
requestAnimationFrame姑且也算是宏任務(wù)吧,requestAnimationFrame在MDN的定義為,下次頁面重繪前所執(zhí)行的操作,而重繪也是作為宏任務(wù)的一個步驟來存在的,且該步驟晚于微任務(wù)的執(zhí)行
# | 瀏覽器 | Node |
---|---|---|
process.nextTick | ? | ? |
MutationObserver | ? | ? |
Promise.then catch finally | ? | ? |
上邊一直在討論 宏任務(wù)、微任務(wù),各種任務(wù)的執(zhí)行。
但是回到現(xiàn)實,JavaScript是一個單進程的語言,同一時間不能處理多個任務(wù),所以何時執(zhí)行宏任務(wù),何時執(zhí)行微任務(wù)?我們需要有這樣的一個判斷邏輯存在。
每辦理完一個業(yè)務(wù),柜員就會問當(dāng)前的客戶,是否還有其他需要辦理的業(yè)務(wù)。_(檢查還有沒有微任務(wù)需要處理)_
而客戶明確告知說沒有事情以后,柜員就去查看后邊還有沒有等著辦理業(yè)務(wù)的人。_(結(jié)束本次宏任務(wù)、檢查還有沒有宏任務(wù)需要處理)_
這個檢查的過程是持續(xù)進行的,每完成一個任務(wù)都會進行一次,而這樣的操作就被稱為Event Loop。_(這是個非常簡易的描述了,實際上會復(fù)雜很多)_
而且就如同上邊所說的,一個柜員同一時間只能處理一件事情,即便這些事情是一個客戶所提出的,所以可以認(rèn)為微任務(wù)也存在一個隊列,大致是這樣的一個邏輯:
const macroTaskList = [ ["task1"], ["task2", "task3"], ["task4"], ] for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) { const microTaskList = macroTaskList[macroIndex] for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) { const microTask = microTaskList[microIndex] // 添加一個微任務(wù) if (microIndex === 1) microTaskList.push("special micro task") // 執(zhí)行任務(wù) console.log(microTask) } // 添加一個宏任務(wù) if (macroIndex === 2) macroTaskList.push(["special macro task"]) } // > task1 // > task2 // > task3 // > special micro task // > task4 // > special macro task
之所以使用兩個for循環(huán)來表示,是因為在循環(huán)內(nèi)部可以很方便的進行push之類的操作(添加一些任務(wù)),從而使迭代的次數(shù)動態(tài)的增加。
以及還要明確的是,Event Loop只是負(fù)責(zé)告訴你該執(zhí)行那些任務(wù),或者說哪些回調(diào)被觸發(fā)了,真正的邏輯還是在進程中執(zhí)行的。
在瀏覽器中的表現(xiàn)在上邊簡單的說明了兩種任務(wù)的差別,以及Event Loop的作用,那么在真實的瀏覽器中是什么表現(xiàn)呢?
首先要明確的一點是,宏任務(wù)必然是在微任務(wù)之后才執(zhí)行的(因為微任務(wù)實際上是宏任務(wù)的其中一個步驟)
I/O這一項感覺有點兒籠統(tǒng),有太多的東西都可以稱之為I/O,點擊一次button,上傳一個文件,與程序產(chǎn)生交互的這些都可以稱之為I/O。
假設(shè)有這樣的一些DOM結(jié)構(gòu):
const $inner = document.querySelector("#inner") const $outer = document.querySelector("#outer") function handler () { console.log("click") // 直接輸出 Promise.resolve().then(_ => console.log("promise")) // 注冊微任務(wù) setTimeout(_ => console.log("timeout")) // 注冊宏任務(wù) requestAnimationFrame(_ => console.log("animationFrame")) // 注冊宏任務(wù) $outer.setAttribute("data-random", Math.random()) // DOM屬性修改,觸發(fā)微任務(wù) } new MutationObserver(_ => { console.log("observer") }).observe($outer, { attributes: true }) $inner.addEventListener("click", handler) $outer.addEventListener("click", handler)
如果點擊#inner,其執(zhí)行順序一定是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout。
因為一次I/O創(chuàng)建了一個宏任務(wù),也就是說在這次任務(wù)中會去觸發(fā)handler。
按照代碼中的注釋,在同步的代碼已經(jīng)執(zhí)行完以后,這時就會去查看是否有微任務(wù)可以執(zhí)行,然后發(fā)現(xiàn)了Promise和MutationObserver兩個微任務(wù),遂執(zhí)行之。
因為click事件會冒泡,所以對應(yīng)的這次I/O會觸發(fā)兩次handler函數(shù)(_一次在inner、一次在outer_),所以會優(yōu)先執(zhí)行冒泡的事件(_早于其他的宏任務(wù)_),也就是說會重復(fù)上述的邏輯。
在執(zhí)行完同步代碼與微任務(wù)以后,這時繼續(xù)向后查找有木有宏任務(wù)。
需要注意的一點是,因為我們觸發(fā)了setAttribute,實際上修改了DOM的屬性,這會導(dǎo)致頁面的重繪,而這個set的操作是同步執(zhí)行的,也就是說requestAnimationFrame的回調(diào)會早于setTimeout所執(zhí)行。
使用上述的示例代碼,如果將手動點擊DOM元素的觸發(fā)方式變?yōu)?b>$inner.click(),那么會得到不一樣的結(jié)果。
在Chrome下的輸出順序大致是這樣的:
click -> click -> promise -> observer -> promise -> animationFrame -> animationFrame -> timeout -> timeout。
與我們手動觸發(fā)click的執(zhí)行順序不一樣的原因是這樣的,因為并不是用戶通過點擊元素實現(xiàn)的觸發(fā)事件,而是類似dispatchEvent這樣的方式,我個人覺得并不能算是一個有效的I/O,在執(zhí)行了一次handler回調(diào)注冊了微任務(wù)、注冊了宏任務(wù)以后,實際上外邊的$inner.click()并沒有執(zhí)行完。
所以在微任務(wù)執(zhí)行之前,還要繼續(xù)冒泡執(zhí)行下一次事件,也就是說觸發(fā)了第二次的handler。
所以輸出了第二次click,等到這兩次handler都執(zhí)行完畢后才會去檢查有沒有微任務(wù)、有沒有宏任務(wù)。
兩點需要注意的:
.click()的這種觸發(fā)事件的方式個人認(rèn)為是類似dispatchEvent,可以理解為同步執(zhí)行的代碼
document.body.addEventListener("click", _ => console.log("click")) document.body.click() document.body.dispatchEvent(new Event("click")) console.log("done") // > click // > click // > done
MutationObserver的監(jiān)聽不會說同時觸發(fā)多次,多次修改只會有一次回調(diào)被觸發(fā)。
new MutationObserver(_ => { console.log("observer") // 如果在這輸出DOM的data-random屬性,必然是最后一次的值,不解釋了 }).observe(document.body, { attributes: true }) document.body.setAttribute("data-random", Math.random()) document.body.setAttribute("data-random", Math.random()) document.body.setAttribute("data-random", Math.random()) // 只會輸出一次 ovserver
這就像去飯店點餐,服務(wù)員喊了三次,XX號的牛肉面,不代表她會給你三碗牛肉面。
上述觀點參閱自Tasks, microtasks, queues and schedules,文中有動畫版的講解
Node也是單線程,但是在處理Event Loop上與瀏覽器稍微有些不同,這里是Node官方文檔的地址。
就單從API層面上來理解,Node新增了兩個方法可以用來使用:微任務(wù)的process.nextTick以及宏任務(wù)的setImmediate。
setImmediate與setTimeout的區(qū)別在官方文檔中的定義,setImmediate為一次Event Loop執(zhí)行完畢后調(diào)用。
setTimeout則是通過計算一個延遲時間后進行執(zhí)行。
但是同時還提到了如果在主進程中直接執(zhí)行這兩個操作,很難保證哪個會先觸發(fā)。
因為如果主進程中先注冊了兩個任務(wù),然后執(zhí)行的代碼耗時超過XXs,而這時定時器已經(jīng)處于可執(zhí)行回調(diào)的狀態(tài)了。
所以會先執(zhí)行定時器,而執(zhí)行完定時器以后才是結(jié)束了一次Event Loop,這時才會執(zhí)行setImmediate。
setTimeout(_ => console.log("setTimeout")) setImmediate(_ => console.log("setImmediate"))
有興趣的可以自己試驗一下,執(zhí)行多次真的會得到不同的結(jié)果。
但是如果后續(xù)添加一些代碼以后,就可以保證setTimeout一定會在setImmediate之前觸發(fā)了:
setTimeout(_ => console.log("setTimeout")) setImmediate(_ => console.log("setImmediate")) let countdown = 1e9 while(countdown--) { } // 我們確保這個循環(huán)的執(zhí)行速度會超過定時器的倒計時,導(dǎo)致這輪循環(huán)沒有結(jié)束時,setTimeout已經(jīng)可以執(zhí)行回調(diào)了,所以會先執(zhí)行`setTimeout`再結(jié)束這一輪循環(huán),也就是說開始執(zhí)行`setImmediate`
如果在另一個宏任務(wù)中,必然是setImmediate先執(zhí)行:
require("fs").readFile(__dirname, _ => { setTimeout(_ => console.log("timeout")) setImmediate(_ => console.log("immediate")) }) // 如果使用一個設(shè)置了延遲的setTimeout也可以實現(xiàn)相同的效果process.nextTick
就像上邊說的,這個可以認(rèn)為是一個類似于Promise和MutationObserver的微任務(wù)實現(xiàn),在代碼執(zhí)行的過程中可以隨時插入nextTick,并且會保證在下一個宏任務(wù)開始之前所執(zhí)行。
在使用方面的一個最常見的例子就是一些事件綁定類的操作:
class Lib extends require("events").EventEmitter { constructor () { super() this.emit("init") } } const lib = new Lib() lib.on("init", _ => { // 這里將永遠(yuǎn)不會執(zhí)行 console.log("init!") })
因為上述的代碼在實例化Lib對象時是同步執(zhí)行的,在實例化完成以后就立馬發(fā)送了init事件。
而這時在外層的主程序還沒有開始執(zhí)行到lib.on("init")監(jiān)聽事件的這一步。
所以會導(dǎo)致發(fā)送事件時沒有回調(diào),回調(diào)注冊后事件不會再次發(fā)送。
我們可以很輕松的使用process.nextTick來解決這個問題:
class Lib extends require("events").EventEmitter { constructor () { super() process.nextTick(_ => { this.emit("init") }) // 同理使用其他的微任務(wù) // 比如Promise.resolve().then(_ => this.emit("init")) // 也可以實現(xiàn)相同的效果 } }
這樣會在主進程的代碼執(zhí)行完畢后,程序空閑時觸發(fā)Event Loop流程查找有沒有微任務(wù),然后再發(fā)送init事件。
關(guān)于有些文章中提到的,循環(huán)調(diào)用process.nextTick會導(dǎo)致報警,后續(xù)的代碼永遠(yuǎn)不會被執(zhí)行,這是對的,參見上邊使用的雙重循環(huán)實現(xiàn)的loop即可,相當(dāng)于在每次for循環(huán)執(zhí)行中都對數(shù)組進行了push操作,這樣循環(huán)永遠(yuǎn)也不會結(jié)束
多提一嘴async/await函數(shù)因為,async/await本質(zhì)上還是基于Promise的一些封裝,而Promise是屬于微任務(wù)的一種。所以在使用await關(guān)鍵字與Promise.then效果類似:
setTimeout(_ => console.log(4)) async function main() { console.log(1) await Promise.resolve() console.log(3) } main() console.log(2)
async函數(shù)在await之前的代碼都是同步執(zhí)行的,可以理解為await之前的代碼屬于new Promise時傳入的代碼,await之后的所有代碼都是在Promise.then中的回調(diào)
小節(jié)JavaScript的代碼運行機制在網(wǎng)上有好多文章都寫,本人道行太淺,只能簡單的說一下自己對其的理解。
并沒有去生摳文檔,一步一步的列出來,像什么查看當(dāng)前棧、執(zhí)行選中的任務(wù)隊列,各種balabala。
感覺對實際寫代碼沒有太大幫助,不如簡單的入個門,掃個盲,大致了解一下這是個什么東西就好了。
推薦幾篇參閱的文章:
tasks-microtasks-queues-and-schedules
understanding-js-the-event-loop
理解Node.js里的process.nextTick()
瀏覽器中的EventLoop說明文檔
Node中的EventLoop說明文檔
requestAnimationFrame | MDN
MutationObserver | MDN
One more thingsBlued前端/Node團隊招人。。初中高都有HC
坐標(biāo)帝都朝陽雙井,有興趣的請聯(lián)系我:
wechat: github_jiasm
mail: jiashunming@blued.com
歡迎砸簡歷
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/108240.html
摘要:檢查宏任務(wù)隊列,發(fā)現(xiàn)有的回調(diào)函數(shù)立即執(zhí)行回調(diào)函數(shù)輸出。接著遇到它的作用是在后將回調(diào)函數(shù)放到宏任務(wù)隊列中這個任務(wù)在再下一次的事件循環(huán)中執(zhí)行。 為什么會寫這篇博文呢? 前段時間,和頭條的小伙伴聊天問頭條面試前端會問哪些問題,他稱如果是他面試的話,event-loop肯定是要問的。那天聊了蠻多,event-loop算是給我留下了很深的印象,原因很簡單,因為之前我從未深入了解過,如果是面試的時...
摘要:從誕生之日起就是一門單線程的非阻塞的腳本語言。這意味著這些線程實際上應(yīng)屬于主線程的子線程。所以嚴(yán)格來講這些線程并沒有完整的功能,也因此這項技術(shù)并非改變了語言的單線程本質(zhì)。函數(shù)執(zhí)行棧和事件隊列 瀏覽器渲染 從耗時的角度,瀏覽器請求、加載、渲染一個頁面,時間花在下面五件事情上:1.DNS 查詢2.TCP 連接3.HTTP 請求即響應(yīng)4.服務(wù)器響應(yīng)5.客戶端渲染 這里重點討論第五個部分,即瀏...
摘要:講的很清晰,看完之后更深一步的理解了事件循環(huán)機制。簡短的概述下總結(jié)是一個宏任務(wù)源,寫在里面的回調(diào)函數(shù)會加到宏任務(wù)隊列中。至此,一輪的事件循環(huán)已經(jīng)執(zhí)行完畢,開啟新的一輪事件循環(huán)。這就是整段代碼執(zhí)行情況的理解。 這篇文章真的是好文。講的很清晰,看完之后更深一步的理解了事件循環(huán)機制。 http://www.jianshu.com/p/12b9... 簡短的概述下總結(jié) setTimeout是一...
摘要:如果執(zhí)行的準(zhǔn)備時間大于了,因為執(zhí)行同步代碼后,定時器的回調(diào)已經(jīng)被放入隊列,所以會先執(zhí)行隊列。 showImg(https://segmentfault.com/img/remote/1460000018998584); 閱讀原文 瀏覽器中的事件輪詢 JavaScript 是一門單線程語言,之所以說是單線程,是因為在瀏覽器中,如果是多線程,并且兩個線程同時操作了同一個 Dom 元素,...
閱讀 2121·2023-04-26 02:23
閱讀 1859·2021-09-03 10:30
閱讀 1427·2019-08-30 15:43
閱讀 1262·2019-08-29 16:29
閱讀 623·2019-08-29 12:28
閱讀 2398·2019-08-26 12:13
閱讀 2357·2019-08-26 12:01
閱讀 2487·2019-08-26 11:56