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