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