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