摘要:前言本文是對(duì)于異步系列第一篇里提到的模型中,所提到的任務(wù)隊(duì)列的展開分析正文說明以下代碼均使用瀏覽器運(yùn)行關(guān)于瀏覽器表現(xiàn)的差異在最后做補(bǔ)充。的函數(shù)執(zhí)行過程,重復(fù)前面的步驟,因此輸出此時(shí)執(zhí)行棧和均為空,存儲(chǔ)著兩個(gè)的回調(diào)函數(shù)。
前言
本文是對(duì)于異步系列第一篇里提到的evenloop模型中,所提到的任務(wù)隊(duì)列(task queues)的展開分析
正文說明:以下代碼均使用chrome瀏覽器運(yùn)行 關(guān)于瀏覽器表現(xiàn)的差異在最后做補(bǔ)充。
引子-奇怪的執(zhí)行順序先看一個(gè)典型的例子:
console.log("script start") // 第一個(gè)異步任務(wù) setTimeout(()=>{ console.log("setTimeout") },0) // 第二個(gè)異步任務(wù) Promise.resolve().then(()=>{ console.log("promise1") }).then(()=>{ console.log("promise2"); }) console.log("script end") // 實(shí)際輸出結(jié)果: // script start // script end // promise1 // promise2 // setTimeout
根據(jù)之前說過的evenloop模型,首先輸出script start和script end沒有什么問題;
但是接下來卻發(fā)現(xiàn):
先執(zhí)行了Promise指定的callback而不是setTimeout的callback。-- Why?
在之前討論evenloop模型時(shí),粗略提到了任務(wù)隊(duì)列有2種類型:microtask queue和macrotask queue,他們的區(qū)別在于:
macrotask的執(zhí)行:是在evenloop的每次循環(huán)過程,取出macrotask queue中可執(zhí)行的第一個(gè)(注意不一定是第一個(gè),因?yàn)槲覀冋f過例如setTimeout可以指定任務(wù)被執(zhí)行的最少延遲時(shí)間,當(dāng)前macrotask queue的首位保存的任務(wù)可能還沒有到執(zhí)行時(shí)間,所以queue只是代表callback插入的順序,不代表執(zhí)行時(shí)也要按照這個(gè)順序)。
microtask的執(zhí)行:在evenloop的每次循環(huán)過程之后,如果當(dāng)前的執(zhí)行棧(call stack)為空,那么執(zhí)行microtask queue中所有可執(zhí)行的任務(wù)
(某些文獻(xiàn)內(nèi)容中 直接把macrotask稱為task,或者某些中文文章中把它們翻譯成"微任務(wù)"和"宏任務(wù)",含義都是相似的:macrotask或者task代表相對(duì)多帶帶占據(jù)evenloop過程一次循環(huán)的任務(wù),而microtask有可能在一次循環(huán)中執(zhí)行多個(gè))
現(xiàn)在回頭來解析前面的例子:
第一次執(zhí)行主函數(shù),輸出script start
遇到setTimeout,將對(duì)應(yīng)的callback插入macrotask queue
遇到promise,將對(duì)應(yīng)的callback插入microtask queue
輸出script end,主函數(shù)運(yùn)行結(jié)束,執(zhí)行棧清空,此時(shí)開始檢查microtask queue,發(fā)現(xiàn)里面有可運(yùn)行的任務(wù),因此按順序輸出promise1和promise2
microtask queue執(zhí)行完,開始新一輪循環(huán),從macrotask queue取出setTimeout任務(wù)并執(zhí)行,輸出setTimeout
結(jié)束,呈現(xiàn)上面的輸出結(jié)果。
常見異步操作對(duì)應(yīng)的回調(diào)函數(shù)任務(wù)類型如下:
macrotask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
microtask: process.nextTick, Promises, Object.observe, MutationObserver
大概可以這樣區(qū)分:和html交互密切相關(guān)的異步操作,一般是macrotasks;由emcascript的相關(guān)接口返回的異步操作,一般是microtasks
如何判斷執(zhí)行順序接下來看一個(gè)更復(fù)雜的例子,幫助理解不同異步任務(wù)的執(zhí)行順序
outerinner點(diǎn)擊inner部分,打開chrome的調(diào)試器,可以看到console打出的結(jié)果是:
click
promise
mutate
click
promise
mutate
timeout
timeout
接下來分析運(yùn)行過程 (建議打開chrome單步調(diào)試,進(jìn)行觀察分析):
點(diǎn)擊inner,觸發(fā)對(duì)應(yīng)的onClick事件,此時(shí)inner對(duì)應(yīng)的onClick函數(shù)進(jìn)入執(zhí)行棧;
運(yùn)行console.log("click"),輸出(1)click;
運(yùn)行setTimeout,macrotask queue添加對(duì)應(yīng)的console函數(shù)
運(yùn)行Promise,此時(shí)microtask queue添加對(duì)應(yīng)的console函數(shù)
運(yùn)行outer.setAttribute,觸發(fā)MutationObserver,microtask queue添加對(duì)應(yīng)的console函數(shù)(前面注明了MutationObserver創(chuàng)建的回調(diào)任務(wù)類型是microtask)
當(dāng)前函數(shù)執(zhí)行完畢,由于執(zhí)行棧清空,此時(shí)開始調(diào)度microtask queue,因此依次輸出(2)promise和(3)mutate,此時(shí)當(dāng)前執(zhí)行棧call stack和microtask queue均為空,但是macrotask queue里依然存儲(chǔ)著兩個(gè)東西--inner的Click觸發(fā)的任務(wù),以及先前setTimeout的回調(diào)函數(shù)。
inner的onclick函數(shù)雖然執(zhí)行完畢,但是由于事件冒泡,緊接著要觸發(fā)outer的onClick的執(zhí)行函數(shù),因此setTimeout的回調(diào)暫時(shí)還無法執(zhí)行。
outer的onClick函數(shù)執(zhí)行過程,重復(fù)前面的2-5步驟,因此輸出(4)click (5)promise (6)mutate
此時(shí)執(zhí)行棧call stack和microtask queue均為空,macrotask queue存儲(chǔ)著兩個(gè)setTimeout的回調(diào)函數(shù)。,根據(jù)evenloop模型,開始分別執(zhí)行這兩個(gè)task,于是輸出了兩個(gè)(7)和(8)timeout
結(jié)束。
再次建議在調(diào)試器查看上面的步驟,尤其要注意觀察call stack、microtask queue macrotask queue的變化,會(huì)更加直觀
在充分理解上面例子的基礎(chǔ)上,我們把點(diǎn)擊inner部分的這個(gè)操作,改成直接在js代碼的末尾加上innner.click(),請(qǐng)問結(jié)果是否一致呢?
先說最終結(jié)果:
click
click
promise
mutate
promise
timeout
timeout
與前一次的結(jié)果完全不同!
接下來再次進(jìn)入調(diào)試分析:由于是直接執(zhí)行inner.click(),這次進(jìn)入inner綁定的onclick函數(shù)時(shí),與前面是有所不同的:
通過chrome調(diào)試器可以看到,此時(shí)的call stack有兩層--除了onClick函數(shù)之外,還有一層匿名函數(shù),這層函數(shù)其實(shí)就是最外層的script,相當(dāng)于window.onload綁定的處理函數(shù)。
這是很關(guān)鍵的一點(diǎn)!??!就是這一個(gè)區(qū)別,導(dǎo)致了整個(gè)執(zhí)行結(jié)果的差異。
因?yàn)榍懊娴睦拥膱?zhí)行順序是:頁面加載后先運(yùn)行了整個(gè)匿名函數(shù)
函數(shù)出棧
點(diǎn)擊時(shí)觸發(fā)inner的onclcik
此時(shí)onClick對(duì)應(yīng)的函數(shù)進(jìn)棧。
兩次執(zhí)行到onclick時(shí)的callstck區(qū)別如圖:
第一次,通過點(diǎn)擊inner觸發(fā)click:
第二次,通過代碼直接觸發(fā)click
接下來分析本次的輸出順序:
重復(fù)前面例子中,步驟2-5,輸出一個(gè)(1)click
inner的onClick函數(shù)執(zhí)行完畢,但是這次執(zhí)行棧并未清空,因?yàn)楫?dāng)前匿名函數(shù)還在執(zhí)行棧里,因此無法開始調(diào)度microtask queue?。。。ㄇ懊嬲f過,microtask queue的調(diào)度必須在當(dāng)前執(zhí)行棧為空的情況下),因此,這時(shí)候會(huì)先進(jìn)入冒泡事件觸發(fā)的onClick
類似的,輸出(2)clcik之后,promise的回調(diào)函數(shù)進(jìn)入microtask queue
運(yùn)行outer.setAttribute,觸發(fā)MutationObserver,但是此時(shí)microtask queue無法再次添加對(duì)應(yīng)的回調(diào)函數(shù)了,因?yàn)橐呀?jīng)有一個(gè)存在的監(jiān)聽函數(shù)在pengding
兩個(gè)onclick執(zhí)行完畢,執(zhí)行棧清空,接下來開始調(diào)度microtask queue,輸出(3)promise (4)mutate (5)promise
此時(shí)當(dāng)前執(zhí)行棧call stack和microtask queue均為空,macrotask queue存儲(chǔ)著兩個(gè)setTimeout的回調(diào)函數(shù)。根據(jù)evenloop模型,開始分別執(zhí)行這兩個(gè)task,于是輸出了兩個(gè)(6)和(7)timeout
結(jié)束
這兩個(gè)例子的對(duì)比,著重說明了一點(diǎn):
結(jié)論
--microtask queue存儲(chǔ)的任務(wù),必須要在當(dāng)前函數(shù)執(zhí)行棧為空時(shí)才會(huì)開始調(diào)度。
完整內(nèi)容可參見html標(biāo)準(zhǔn)中的8.1.4部分macrotask會(huì)按順序執(zhí)行,并且有可能被中途插入瀏覽器render,例如上面的冒泡事件
microtask的執(zhí)行有兩個(gè)條件:
在每個(gè)macrotask結(jié)束之后
當(dāng)前call stack為空
ps:瀏覽器差異上述代碼在chrome的瀏覽器下測(cè)試結(jié)果,可能和在某些版本的firefox和ie瀏覽器下不一致,在某些瀏覽器中可能會(huì)把promise的回調(diào)函數(shù)當(dāng)做mascrotask,但是:
普遍的共識(shí)把 Promise當(dāng)做是miscrotask,并且有比較充分的理由:如果把promose當(dāng)做是task(即mascrotask)將會(huì)導(dǎo)致一些性能問題--因?yàn)閠ask的調(diào)度是可以被其他task相關(guān)的任務(wù)如Render打斷,還會(huì)因?yàn)榕c其他任務(wù)源的交互導(dǎo)致不確定性。參考文獻(xiàn)Tasks, microtasks, queues and schedules
HTML Living Standard
如果覺得寫得不好/有錯(cuò)誤/表述不明確,都?xì)g迎指出
如果有幫助,歡迎點(diǎn)贊和收藏,轉(zhuǎn)載請(qǐng)征得同意后著明出處。如果有問題也歡迎私信交流,主頁有郵箱地址
如果覺得作者很辛苦,也歡迎打賞~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/109389.html
摘要:前文該系列下的前幾篇文章分別對(duì)不同的幾種異步方案原理進(jìn)行解析,本文將介紹一些實(shí)際場(chǎng)景和一些常見的面試題。流程調(diào)度里比較常見的一種錯(cuò)誤是看似串行的寫法,可以感受一下這個(gè)例子判斷以下幾種寫法的輸出結(jié)果辨別輸出順序這類題目一般出現(xiàn)在面試題里。 前文 該系列下的前幾篇文章分別對(duì)不同的幾種異步方案原理進(jìn)行解析,本文將介紹一些實(shí)際場(chǎng)景和一些常見的面試題。(積累不太夠,后面想到再補(bǔ)) 正文 流程調(diào)度...
摘要:只要指定過回調(diào)函數(shù),這些事件發(fā)生時(shí)就會(huì)進(jìn)入任務(wù)隊(duì)列,等待主線程讀取。三主線程從任務(wù)隊(duì)列中讀取事件,這個(gè)過程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為事件循環(huán)。 一、任務(wù)隊(duì)列 同步任務(wù)與異步任務(wù)的由來 單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng),后一個(gè)任務(wù)就不得不一直等著。 如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過來,倒也算了,但是很多時(shí)候C...
摘要:心塞塞根據(jù)規(guī)范,事件循環(huán)是通過任務(wù)隊(duì)列的機(jī)制來進(jìn)行協(xié)調(diào)的。等便是任務(wù)源,而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)回調(diào)函數(shù)。然后當(dāng)前本輪的結(jié)束,主線程可以繼續(xù)取下一個(gè)執(zhí)行。 依然是:經(jīng)濟(jì)基礎(chǔ)決定上層建筑。 說明 首先,旨在搞清常用的同步異步執(zhí)行機(jī)制 其次,暫時(shí)不討論node.js的Event Loop執(zhí)行機(jī)制,以下關(guān)于瀏覽器的Event Loop執(zhí)行機(jī)制 最后,借鑒了很多前輩的研究文...
摘要:如果對(duì)語法分析和預(yù)編譯,還有疑問引擎執(zhí)行的過程的理解語法分析和預(yù)編譯階段。參與執(zhí)行過程的線程分別是引擎線程也稱為內(nèi)核,負(fù)責(zé)解析執(zhí)行腳本程序的主線程例如引擎。以上便是引擎執(zhí)行宏任務(wù)的整個(gè)過程。 一、概述 js引擎執(zhí)行過程主要分為三個(gè)階段,分別是語法分析,預(yù)編譯和執(zhí)行階段,上篇文章我們介紹了語法分析和預(yù)編譯階段,那么我們先做個(gè)簡(jiǎn)單概括,如下: 1、語法分析: 分別對(duì)加載完成的代碼塊進(jìn)行語法...
摘要:如果對(duì)語法分析和預(yù)編譯,還有疑問引擎執(zhí)行的過程的理解語法分析和預(yù)編譯階段。參與執(zhí)行過程的線程分別是引擎線程也稱為內(nèi)核,負(fù)責(zé)解析執(zhí)行腳本程序的主線程例如引擎。以上便是引擎執(zhí)行宏任務(wù)的整個(gè)過程。一、概述 js引擎執(zhí)行過程主要分為三個(gè)階段,分別是語法分析,預(yù)編譯和執(zhí)行階段,上篇文章我們介紹了語法分析和預(yù)編譯階段,那么我們先做個(gè)簡(jiǎn)單概括,如下: 1、語法分析: 分別對(duì)加載完成的代碼塊進(jìn)行語法檢驗(yàn),語...
閱讀 2543·2021-11-15 11:36
閱讀 1304·2019-08-30 15:56
閱讀 2404·2019-08-30 15:53
閱讀 1120·2019-08-30 15:44
閱讀 730·2019-08-30 14:13
閱讀 1071·2019-08-30 10:58
閱讀 558·2019-08-29 15:35
閱讀 1379·2019-08-29 13:58