很多地方都已經介紹了JavaScript在瀏覽器是如何被執行的,這裡介紹一下WebKit是如何?的。主要涉及JS的async,defer及普通指令碼的解析與執行過程的代碼實現。
1. 概要說明
先概要說明一下瀏覽器如何執行JavaScript的。 首先瀏覽器的頁面解析器(Document Parser)遇到<script>就會發起下載(指令碼內容在頁面內的就不用下載了)。然後針對不同情況執行的方式有所不同:
. async (在script標籤中啟用了async屬性)
這是非同步執行,下載時不會阻塞Document Parser, 當JavaScript被載入完成後就會開始執行。
. defer (在script標籤中啟用了defer屬性)
這個是延遲執行,下載時同樣不會阻塞Document Parser, 會放到Document Parser完成頁面解析後才會執行。
. 對於引用外部的指令檔,下載時Document Parser會被阻塞至指令碼執行完。
. 如果頁面中有Style Sheet還沒有解析成功, 則指令碼會被阻塞直到Style Sheet下載並解析完成。
因為阻塞會嚴重影響頁面解析的效能,所以也是瀏覽器最佳化的重點:
a. WebKit引入Preload Scanner允許在下載時嘗試繼續處理後面的資源。 [LINK] 不過隨著網路的提速,這個最佳化的效果應逐漸減弱。
b. FireFox實現非同步HTML解析,WebKit也2013年年初實現了一個輕量級版本(只將tokenizer多線程化)。
2. WebCore中執行JavaScript 2.1 主要功能劃分
WebCore中關於JavaScript的元素的解析與執行,個人把相關類分成兩個主要的類別:
頁面解析功能和JS執行功能。
頁面解析類別的側重於在HTMLDocumentParser解析頁面過程中管理指令碼的建立、規劃執行策略等。這一部分細節功能比較複雜,在W3C中有專門的定義,WebKit中的實現很多地方也註上了所參考的章節。
JS執行類別基於Document和Frame來管理JS執行環境,並同JSC協作執行JS指令碼。
下面是一個總覽, 黃色背景的類屬於為頁面解析類別,紅色背景的部分則歸到執行類別。
*主要類的代碼基本集中在bindings/js目錄下。
2.2 JS元素的執行
這裡的執行不包括頁面對JS執行的策略,主要針對具體的執行過程。
JSMainThreadExecState 負責最終對接JSC模組執行JS指令碼。在系統中是一個單例的對象,也就是保證單進程與JSC協作。這同樣也是JS的一個限制點。
ScriptController 維護一個JavaScriptCore執行環境,在必要時調用JSMainThreadExecState執行。
ScriptElement 代表了一個具體的script元素,包括JavaScript, SVG等。
再附上一張時序圖來協助瞭解它們間的關係, (其中指令碼的執行決策是由HTMLScriptRunner來發起的):
2.3 JS元素的解析與管理
在WebKit內部,頁面解析與指令碼控制的操作主要是在HTMLDocumentParser,HTMLScriptRunner和ScriptElement中實現的。
2.3.1 JS解析
從下面這張經典的圖說起:
這張圖源自W3C的官方定義, 與主文相關的是說明JS之所以會導致頁面解析阻塞正是因為它允許指令碼使用document.write()改變頁面內容,會造成DOM樹可能會需要重新解析(reparser)。所以非但瀏覽器要做些最佳化,JS開發人員也要注意JS的實現方法,比如編寫non-block
script。
另外不但JS阻塞Parser, Style Sheet也會阻塞JS。如果JS裡包含了對某個元素的顯示內容的檢測,在CSS還沒有載入解析完成的情況下,肯定得不到正確結果。所以對應在WebKit也對應出了不同的定義。
在HTMLScriptRunner有兩個重要的成員變數:
m_scriptsToExecuteAfterParsing -> 由requestDeferredScript()添加。
m_parserBlockingScript
-> 由requestParsingBlockingScript添加。
根據名字也可以看出來前者是儲存defer指令碼,後者是儲存一般的指令碼的。至於async則是在下載完成後觸發的,稍後說明。
2.4 JS運行策略
之前說到不同的JS載入屬性的執行方法會不一樣:
async -> 是由ScriptRunner::notifyScriptReady在指令碼下載完成時執行的。
defer -> 是由HTMLDocumentParser::notifyFinished()在頁面解析完成時發起執行的。
其它的JS指令碼則在處理token時由HTMLDocumentParser::canTakeNextToken來檢測並發起執行的。
2.4.1一般JS指令碼的執行
一般沒有帶async和defer屬性的JS指令碼,被稱為解析阻塞指令碼(Parsing Blocking Scripts),則由HTMLScriptRunner::executeParsingBlockingScripts()負責執行的。
一個典型的調用方式如下:
比如當新解析到一個Token時,Document Parser就會觸發一個canTakeNextToken來檢查一些狀態,其中一項就是看看指令碼是否可以運行,如果條件合適就會執行相應的指令碼。所謂條件合適,可以在它會調用的HTMLScriptRunner::isPendingScriptReady裡看到(函數也很好理解,就是看之前掛起的指令碼是不是又可以執行了) :
bool HTMLScriptRunner::isPendingScriptReady(const PendingScript& script){ m_hasScriptsWaitingForStylesheets = !m_document->haveStylesheetsLoaded(); if (m_hasScriptsWaitingForStylesheets) return false; if (script.cachedScript() && !script.cachedScript()->isLoaded()) return false; return true;}
其中等待的就是兩個條件:
1. 是不是正在載入CSS.
2. 指令碼是不是有信賴的指令碼還在載入.
下面再介紹一個入口。因為有一部分指令碼是等待CSS載入完成後才會執行的,所以在CSS載入完成會一次調用過程如下:
2.4.2 async與defer指令碼的執行
下面就附兩張圖來說明兩者的執行過程,中間省略一些不重要的步驟。
async的執行過程, 注意是由資源載入觸發:
defer指令碼的執行過程,除了這個之外,還有一些異常的考慮,比如在HTMLScriptRunner析構時也會進行處理。
2.4.3 進一步閱讀代碼指引
以HTMLScriptRunner的runScript為例,它是由HTMLScriptRunner::execute()調用的,其實對應於W3C定義的標準流程: Running
a script:
這一部分的涉及到很多的細節,可以參考W3C的定義進一步閱讀代碼。
http://www.whatwg.org/specs/web-apps/current-work/multipage/scripting-1.html
ScriptElement::prepareScript() -> http://dev.w3.org/html5/spec/Overview.html#prepare-a-script
WebKit在重要函數裡也標註了對應標準文檔的章節,看代碼前可以先閱讀這部分的說明。
算是做個筆記吧,有時間再進一步研究。
轉載請註明出處:http://blog.csdn.net/horkychen
參考:WebKit研究
相關的UML圖可以到這裡下載 :WebKit Documentation on GitHub
系列索引:
基礎篇 (一)JSC與WebCore
基礎篇(二)解譯器基礎與JSC核心組件
基礎篇(三)從指令碼代碼到JIT編譯的代碼實現
基礎篇(四) 頁面解析與JavaScript元素的執行
進階篇(一) SSA (static single assignment)
進階篇(二) 類型推導(Type Inference)
進階篇(三) Register Allocation & Trampoline