script標籤與event loop在W3C規範及瀏覽器中的表現

前言

本文主要對W3C規範中關於script標籤event loop相關的篇幅作了簡單的探討,針對一些必要的相關概念進行了適當的標註和說明。雖然以前接觸過,但都過於零散,但願藉此機會,可以對這些概念可以一個稍微全面一點的認識,也但願和你們進行交流。因爲知識的深度和廣度以及英語水平的不足,若有錯誤,還望包涵指正。javascript

小波折

雖然以前查過W3C和WHATWG的關係,可是翻譯得差很少的時候有個問題去WHATWG提了issue,才被domenic大大告知我可能看了"假規範"- -(具體可參考連接1連接2Fork tracking),最新的規範在這,大部分仍是基本一致的,新增了一些好比type=module的內容等等,還有排版呀,有的描述等等有一些變化,有興趣的能夠去看看。css

HTML解析

瀏覽器HTML解析過程以下html

The exact processing details for these attributes are, for mostly historical reasons, somewhat non-trivial, involving a number of aspects of HTML.The implementation requirements are therefore by necessity scattered throughout the specification.html5

能夠看到,規範也提到了規範只是一個參考,具體實現因人而異。在測試中,我列出如下發現和期待討論的話題,但願對本身和他人都能起到幫助:java

Script標籤

關於script標籤基本信息的一些描述這裏再也不過多介紹,本身有幾個比較關心的點在如下列出。node

defer,async屬性

對於普通腳本,defer腳本,async腳本,有以下總結:css3

1.對於普通的腳本,有兩點須要注意。git

第一:並非在fetch的時候徹底「阻止」後續標籤的解析。咱們從timeline能夠看到,在第一次praseHTML的最開始,就已經將頁面所需的全部靜態資源請求send出去了(具體可參考瀏覽器預解析加載機制)。因此腳本是沒法「阻止」後續標籤中引用外部資源的請求不被髮送的。而且在解析這個腳本到finish load這段時間,還有不少其餘操做,如擴展程序的腳本執行,某些VM語句執行,install這一腳本以前的腳本中的定時器等等。github

第二:fetch以後接收完全部數據包最後完成finish load以後,並非當即執行這個腳本內的內容。而是先要判斷這個腳本在全部腳本中的順序,必須肯定這個腳本以前的全部普通腳本執行完畢後,纔會執行這個腳本。web

script標籤處理模型

處理模型擁有如下7種狀態屬性:

"already started" ->> "parser-inserted" ->> "non-blocking" ->> "ready to be parser-executed" ->> "the script’s type" ->> "is from an external file" ->> "the script’s script"

最後一步也就是異步將預處理腳本(見下)的結果設置爲腳本的script屬性的值,不管這個值是正確的仍是錯誤的,都應標記腳本爲ready狀態,這意味着這以後能夠觸發其餘行爲。瀏覽器推遲load事件直到全部的腳本都處於ready狀態。

關於這些狀態的描述內容並很少,好比初始時尚未"already started",HTML解析器解析到以後當即置爲"already started",初始時沒有"parser-inserted",當HTML解析器把節點插入到父節點的時候,置爲"parser-inserted",當HTML解析器在建立節點對象的時候默認是"non-blocking",當HTML解析器把節點插入到父節點的時候,置爲"blocking"(實際是設置爲false,便於理解故做此翻譯,不要打我。。),若是腳本有async屬性,那麼又置爲"non-blocking"避免阻塞解析,等等等等。

具體要了解的話建議查閱文檔。這裏咱們探討下最關鍵的部分-預處理腳本(原文爲prepare a script,感受翻譯成準備,預備都不太合適,故做此翻譯,若是有更好的翻譯還但願指正),"the script's type","from an external file","the script's script"都是在這一階段肯定的:

預處理腳本

當一個未被標記爲"parser-inserted"的腳本元素經歷如下3個事件中的任意一個時,瀏覽器必須當即預處理這個腳本元素:

  • 1.在dom節點中順序先於(先因而指按照前序進行深度優先遍歷的時候)這個腳本的腳本被插入到dom樹以後,這個腳本元素被插入到文檔中。

  • 2.在全部腳本元素都被插入完畢後,這個腳本元素在文檔中且有其餘節點被插入到這個腳本元素中。

  • 3.腳本元素已經在文檔中而且以前沒有src屬性,可是如今被設置了src屬性。

爲了預處理一個腳本,瀏覽器必須進行如下步驟:

前面1-18步主要考慮的是不須要執行或者說不符合執行條件的時候就中斷預處理過程從而不執行這個腳本。好比發現尚未"already started",好比沒有src屬性且腳本的內容爲空或者只有註釋,腳本元素沒有在文檔中,type和language屬性不符合規範,用戶禁用了JS等等。除了這些以外,還有些諸如腳本有charset則設置,沒有就用文檔自己的charset。還有有些只是規範有說起,可是沒有瀏覽器或者不是全部瀏覽器都實現了,好比for,event,nonce屬性等等。另外還有一些其餘的考慮,這裏就不一一贅述了,詳細的能夠參考規範。

下面着重來看一下19-20步
第19步:若是腳本元素沒有src屬性,則進行下列的步驟:

  • Let source text等於yourScriptElement.text的值。

  • 將腳本的type屬性設置爲"classic"

    • Let script做爲用source textsettings建立的腳本的結果。

    • 設置the script’s script爲上一步的script

    • 讓腳本處於ready狀態

第20步:而後,選擇符合下列情形的第一個進行執行:

Type 1:

the script’s type 是否有src屬性 是否有defer屬性 是否有async屬性 其餘條件
"classic" 元素已經"parser-inserted"

將腳本元素添加到將要執行的腳本的集合的末尾。

當腳本處於ready狀態的時候,設置腳本元素的"ready to be parser-executed"標記。解析器將處理執行這個腳本。

Type 2:

the script’s type 是否有src屬性 是否有defer屬性 是否有async屬性 其餘條件
"classic" 元素已經"parser-inserted"

腳本元素爲"等待解析阻塞的腳本"(見步驟末尾)的狀態,同一時刻只能有一個這樣的腳本存在。當腳本處於ready狀態的時候,設置腳本元素的"ready to be parser-executed"標記。解析器將處理執行這個腳本。

Type 3:

the script’s type 是否有src屬性 是否有defer屬性 是否有async屬性 其餘條件
"classic" 是或者否(意思爲是或者否都是同樣的) 元素上沒有"non-blocking"標記

儘快當預處理腳本一開始的時候按順序將腳本元素添加到將要執行的腳本的集合的末尾。

當腳本爲ready狀態的時候,進行下面的步驟:

  • 1.若是這個腳本如今不是將要執行的腳本的集合的第一個元素,則標記腳本爲ready,可是中斷剩餘的步驟,不執行這個腳本。

  • 2.執行腳本。

  • 3.移除將要執行的腳本的集合中的第一個元素。

  • 4.若是將要執行的腳本的集合仍然不爲空且第一個元素被標記爲ready,那麼跳回到第2步。

Type 4:

the script’s type 是否有src屬性 是否有defer屬性 是否有async屬性 其餘條件
"classic" 是或者否 是或者否 不適用

儘快當預處理腳本一開始的時候將腳本元素添加到將要執行的腳本的集合的末尾。

當腳本爲ready狀態的時候,執行腳本並將它從集合中移除。

Type 5:

the script’s type 是否有src屬性 是否有defer屬性 是否有async屬性 其餘條件
"classic" 是或者否 是或者否 元素已經"parser-inserted",XML或者HTML解析器的script-nesting-level比建立這個腳本的低或者相等。建立這個腳本的解析器的文檔有css正在阻塞腳本執行

腳本元素爲"等待解析阻塞的腳本"的狀態,同一時刻只能有一個這樣的腳本存在。設置腳本元素的"ready to be parser-executed"標記。解析器將處理執行這個腳本。

Type 6(其餘情形):

當即執行這個腳本,即便有其餘腳本正在執行。

總共就這6種情形,下面有一個上面提到的概念的補充說明
等待解析阻塞的腳本:
若是一個阻塞瞭解析的腳本元素在它中止阻塞解析前被移動到了另外一個document中,儘管如此,它仍然會阻塞解析直到形成它阻塞的緣由消除。(例如,若是這個腳本元素因爲有一個css阻塞了它而變成一個等待解析阻塞的腳本,可是而後這個腳本在css加載完畢前被移動到了另外一個文檔中,這個腳本仍然會阻塞解析直到css加載完畢(可是阻塞的是另外那個文檔的解析了),但在這段期間,原來文檔的腳本執行和HTML解析是暢通的)

Event Loop

在規範中user agents指的是實現了這些規範的應用。爲了更好的敘述,如下咱們暫且用瀏覽器來代替這一描述。

爲了協調事件,用戶接口,腳本,渲染,網絡等等,瀏覽器必須使用event loops。對於event loops,它有兩種類型,一種是針對瀏覽器上下文(請務必先了解這一律念)而言,另外一種是針對Wokrer而言。因爲對Worker不太熟悉,咱們這裏也主要探討瀏覽器相關的東西,因此如下都再也不敘述Worker相關內容。

一個event loop一個或者多個任務隊列。一個任何隊列是一系列有序的任務集合,這樣的隊列是經過下面這些算法來工做的:

  • Events:一般對於專用任務而言,dispatch一個Event對象給一個特定的EventTarget對象。另外,並非全部的事件都是經過任務隊列來dispatch(哪些不是呢,可參考區別)。

  • Parsing:HTML解析器將一個或多個字符轉換爲token表並處理,這個過程是一個典型的task。

  • Callbacks:調用一個回調函數常,常適用於專有任務。

  • Using a resource:當fetch一個資源的時候,若是fetch發生在一個非阻塞的方法,一旦資源的部分或者所有是可用的,也會被看成一個任務執行(即timeline中的receive data和finish loading)。

  • Reacting to DOM manipulation:爲了響應dom變化也會致使一些元素產生task。如當一個元素被插入到文檔中的時候。(意思就是說插入以後會致使瀏覽器從新計算佈局,渲染,一些監聽節點變化的事件也會被觸發,這些都是task)

每個在瀏覽器上下文的event loop中的task都與Document對象(準確的說是實現了Document接口的對象,規範也說起過爲了便於敘述不採用這種準確的說法,由於太長)相關聯。若是某個task被加入了某個元素的context的隊列,那麼這個document對象就是這個元素的node document。若是某個task被加入了某個瀏覽器上下文的context的隊列,那麼在入隊列的時候,這個document對象就是瀏覽器上下文的active document。若是某個task是經過腳本或者是針對腳本的,那麼這個document對象就是經過腳本的配置對象指定的responsible document(如今想一想responsible這個詞在這裏仍是挺有意思,由於純靜態頁面的document是不須要對任何東西負責的)。

當瀏覽器將一個task加入隊列的時候,它必須將這個task加入相關的event loop中的某一個任務隊列。

每個task在定義時都會有指定的task source(一共有4種,DOM manipulation task source,user interaction task source,networking task source,history traversal task source)。全部來自一個特定的task source的task都必須被添加到一個特定的相同的event loop(例如Document對象產生的回調函數,觸發在Document對象上的mouseover事件,Document中等待解析的任務等等,他們都有相同的事件源-Document),可是不一樣來自不一樣task source的task也許會被添加到不一樣的任務隊列。

例如,瀏覽器也許有一個針對鼠標和鍵盤的任務隊列(它們都來自user interaction這一task source)和其餘的任務隊列。那麼相對其餘任務隊列而言,瀏覽器也許會給鼠標和鍵盤事件更高的優先級,來保持響應與用戶的交互,可是這又不會飢餓其餘任務隊列。而且毫不會未來自同一task source的事件顛倒次序執行(意思就是task必須按照它添加時的順序去執行)。

每個event loop都有一個當前執行任務。初始時爲null。它被用做處理reentrancy(可重入性,相似於generator,在內聯腳本中直接使用document.write就是這樣,由於這樣是把write的參數寫到以前的input stream(就是還未解析的字節流)裏面)。每個event loop也有一個performing a microtask checkpoint 的flag,初始時爲false。它被用做阻止對perform a microtask checkpoint這個算法的可重入性調用。

  • 關於microtask:每個event loop都有一個microtask隊列,處於microtask隊列而不是普通的task隊列中的task就叫作microtask。這裏有兩種類型的microtask,一種是單一回調函數microtask,一種是複合microtask。注意,規範中只針對單一回調函數microtask有具體描述。

一個event loop在它存在的期間必須不斷重複如下步驟:

1.取出某一個任務隊列隊列頭的任務(若是存在的話)。若是與瀏覽器上下文的event loop相關聯的Documents對象不是fully active狀態,那麼忽略這個task。瀏覽器也許會選擇任何一個任務隊列。若是沒有task能夠取的話,跳到第6步。

2.將event loop的當前運行任務設置爲上一步選擇到的task。

3.運行這個task。

4.將event loop的當前運行任務設置爲null

5.將第3步中運行的task從它的任務隊列中移除。(這也說明以前取任務時進行的隊列操做是peek,而不是poll)

6.執行一個microtask checkpoint操做。由於有點多,避免混亂我寫在這7個步驟完畢後的位置。

7.更新渲染:若是這個event loop是瀏覽器上下文的event loop而非Worker的event loop,那麼執行以下步驟:

  • Let now等於now()方法的返回值。(能夠理解爲timeline中的start time)

  • Let docs等於與這個event loop相關聯的Document對象集合。這個集合是隨意排序的,可是要遵循必定的原則,具體能夠參照規範。簡單舉例來講,A這個Document嵌套了B和C,B嵌套了D。那麼順序便可以是A,B,C,D也能夠是A,B,D,C。只要保證C在B後面,B,C在A後面,D在B後面就行。

  • 迭代docs,對於其中的每一個doc。若是這裏存在一個頂級的瀏覽器上下文B(頂級就是指嵌套瀏覽器上下文狀況下最祖先的那個瀏覽器上下文,形象一點的描述可參考連接且不會從此次更新渲染中受益,那麼將docs中全部瀏覽器上下文的頂級瀏覽器上下文爲B的Document對象移除。

    • 一個頂級瀏覽器上下文是否會從渲染更新中受益取決與幾個方面,如更新頻率。舉例來講,若是瀏覽器嘗試60HZ的刷新頻率,那麼這些步驟只有在每16.7ms內纔是有意義的。若是瀏覽器發現一個頂級瀏覽器上下文沒法維持這個頻率,它也許會將docs集合中的全部document對應的刷新頻率下調到30HZ,而不是偶爾下調頻率。(規範並不強制規定任何特定的模型用於什麼時候更新渲染),相似的,若是一個頂級瀏覽器上下文是在background中(不太明白,猜想是dispaly:none之類的意思),那麼瀏覽器也許會下調到4HZ,甚至更低。

    • 另外一個關於瀏覽器可能會跳過更新渲染的例子是確保某些task在某些task以後被當即執行,這伴隨着僅僅是microtask checkpoints的交替。(或者沒有這些交替,例如requestAnimationFrame中animation幀的回調函數交替)。例如,瀏覽器也許但願合併定時器回調函數,而不但願在合併的時候存在渲染更新。

  • 若是有一個瀏覽器認爲不會從渲染更新中受益的嵌套的瀏覽器上下文B,那麼從docs中移除那些瀏覽器上下文爲B的元素。

    • 正如頂級瀏覽器上下文同樣,對於嵌套的瀏覽器行下午,不少因素也會影響到它是否會從更新渲染中受益。例如,瀏覽器也許但願花費較少的資源渲染第三方的內容,特別是當前不可見的內容或者是受限制的內容。在這一的例子中,瀏覽器也許會決定不多或者根本不對這些內容更新渲染。

  • 對於docs中每一個fully active的Document對象,觸發resize

  • 對於docs中每一個fully active的Document對象,觸發scroll

  • 對於docs中每一個fully active的Document對象,觸發媒體查詢和提交變化

  • 對於docs中每一個fully active的Document對象,運行CSS animations併發送事件。

  • 對於docs中每一個fully active的Document對象,運行全屏渲染步驟

  • 對於docs中每一個fully active的Document對象,運行animations回調函數

  • 對於docs中每一個fully active的Document對象,更新渲染或者用戶接口,和瀏覽器上下文來反應當前的狀態。

9.返回到第1步繼續執行。

接上面提到的第6步,執行microtask checkpoint操做以下:

當一個算法須要將一個microtask加入隊列時,它必須被追加到相關的event loop的microtask 隊列。這個microtask的task source就被叫作microtask task source。

將一個microtask移動到普通的任務隊列是頗有可能的,若是發生這樣的移動的話,在它的初次運行時,將執行spins the event loop步驟。

當瀏覽器去執行一個microtask checkpoint的時候,若是這個performing a microtask checkpoint的falg爲false,那麼瀏覽器必須執行如下步驟:

1.將這個flag置爲true

2.若是event loop的microtask隊列爲空,則跳到第8步:

3.取出microtask隊列頭的元素。

4.將event loop的當前運行任務設置爲上一步取出的task。

5.運行這個task。

  • 注意:這也許會涉及調用回調函數,最後會調用清理步驟,在清理步驟中也許又會執行microtask checkpoint操做,致使無終止條件的遞歸,這就是爲何咱們須要用這個flag去避免這一狀況。

6.設置event loop的當前運行任務爲null

7.從microtask隊列中移除上面運行的這個task。而後返回到第2步。

8.對於每個responsible event loop爲這個event loop的環境配置對象,notify about rejected promises

9.將flag置爲false

Timeline相關

就像咱們用迅雷同時下載10個文件同樣,假設咱們是下行速度是1M/s,那麼顯然不可能10個資源每一個的下載速度都是100kb/s,由於每一個資源的資源熱度是不一樣的,因此有的是500kb/s,而有的可能只有20kb/s,有的甚至沒法下載。

對於瀏覽器而已也是相似的道理,瀏覽器的資源調度算法以及每一個時間段的網絡狀況決定了下載資源的順序,所花費的精力等等。以chrome的資源獲取優先級算法爲例,咱們不難看出,在獲取到html以後,css的請求優先級是最高的,由於對於如今的web頁面來講,沒有css的後果可能遠遠大於沒有其餘資源。對於腳本中發起的請求如經過接口獲取數據等則爲high,對於普通的js而已,優先級爲medium,普通的圖片和async腳本都爲low等等等等,隨着時間的推移,這個算法確定也會發生相應的變化來提高那個時候的應用體驗。

關於這些點在network中與之相關的莫過於Queueing和Stalled屬性了:

  • Queueing. The browser queues requests when:

    • There are higher priority requests.

    • There are already six TCP connections open for this origin, which is the limit. Applies to HTTP/1.0 and HTTP/1.1 only.

  • Stalled: The request could be stalled for any of the reasons described in Queueing.

因此瀏覽器最開始會按照html中資源出現的順序發送請求去獲取,可是資源的接收順序卻不必定是按照這個順序。一個請求發出去以後,後面又來了一個請求,而這個請求的優先級比當前的要高,那麼極可能就會先去接收這個優先級更高的資源的數據。而對於優先級相同的多個資源,則極可能採用你接收一段數據,我接收一段數據這樣的方式交叉運行。也就是咱們經常看到的頁面中的圖片加載的時候每每是多個圖片同時慢慢從白屏到加載完畢,而不是一個加載完畢後再加載另外一個。

另外前面已經提到過了,對於普通腳本則是確定會按照html中的順序執行的,也就是說若是腳本a只有500kb,而在他後面的腳本b只有1kb,那麼即便腳本b獲取所有字節後完成finish load也不能當即執行,必須等到腳本a獲取所有字節後且執行完畢後它才能執行。而若是a和b都是async的腳本化則沒必要遵循這一原則,誰先獲取到誰就先執行。爲何呢,由於async設計的本意就是爲了抽離與頁面無關的邏輯的,它們之間也不該該存在連貫性和依賴性,然後面的普通腳本更不用說了,更不該依賴它們去工做。

因此後面連接提到的視頻中提問者說只要不操做dom和獲取dom,就應該把這些公共代碼提取出來放在head中async引入來達到性能優化的效果,實際上是不穩當的。好比loadsh就符合這個要求,咱們顯然不能這麼作,一是由於lodash體積太大,沒法保證在body尾部用到lodash的代碼所處的腳本必定晚於lodash後執行,二是因爲網絡緣由,就是lodash是一個只有1kb的資源,也很難保證。

寫在結尾

此次閱讀規範的過程,瞭解了不少知識,也早已超出了當初想要得到的知識,這即是學習的樂趣。固然也有不少地方花了很長時間才弄清楚究竟是表達的什麼意思,也還存留一些問題到目前也仍未理解,你們有不明白或者以爲錯誤的地方但願多多交流,也但願隨着歲月,再來回頭探索的時候可以明白。

閱讀更多

the-javascript-event-loop-explained

從Chrome源碼看瀏覽器如何構建DOM樹

從Chrome源碼看瀏覽器的事件機制

瀏覽器如何構建dom樹(chrome官方文檔,另外裏面有配套的視頻,很是不錯)

相關文章
相關標籤/搜索