新聞中心
這幾天下了Chrome的源碼,安裝了一個debug版的Chromium研究了一下,雖然很多地方都一知半解,但是還是有一點收獲,將在這篇文章介紹DOM樹是如何構(gòu)建的,看了本文應(yīng)該可以回答以下問題:

金沙ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場景,ssl證書未來市場廣闊!成為成都創(chuàng)新互聯(lián)的ssl證書銷售渠道,可以享受市場價格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18982081108(備注:SSL證書合作)期待與您的合作!
- IE用的是Trident內(nèi)核,Safari用的是Webkit,Chrome用的是Blink,到底什么是內(nèi)核,它們的區(qū)別是什么?
- 如果沒有聲明會造成什么影響?
- 瀏覽器如何處理自定義的標(biāo)簽,如寫一個?
- 查DOM的過程是怎么樣的?
先說一下,怎么安裝一個可以debug的Chrome
1. 從源碼安裝Chrome
為了可以打斷點debug,必須得從頭編譯(編譯的時候帶上debug參數(shù))。所以要下載源碼,Chrome把***的代碼更新到了Chromium的工程,是完全開源的,你可以把它整一個git工程下載下來。Chromium的下載安裝可參考它的文檔, 這里把一些關(guān)鍵點說一下,以Mac為例。你需要先下載它的安裝腳本工具,然后下載源碼:
fetch chromium --no-history
–no-history的作用是不把整個git工程下載下來,那個實在是太大了?;蛘呤侵苯訄?zhí)行g(shù)it clone:
git clone https://chromium.googlesource.com/chromium/src
這個就是整一個git工程,下載下來有6.48GB(那時)。博主就是用的這樣的方式,如果下載到***提示出錯了:
fatal: The remote end hung up unexpectedly fatal: early EOF fatal: index-pack failed
可以這樣解決:
git config --global core.compression 0 git clone --depth 1 https://chromium.googlesource.com/chromium/src
就不用重頭開始clone,因為實在太大、太耗時了。
下載好之后生成build的文件:
gn gen out/gn --ide=xcode
–ide=xcode是為了能夠使用蘋果的XCode進(jìn)行可視化進(jìn)行調(diào)試。gn命令要下載Chrome的devtools包,文檔里面有說明。
準(zhǔn)備就緒之后就可以進(jìn)行編譯了:
ninja -C out/gn chrome
在筆者的電腦上編譯了3個小時,firfox的源碼需要編譯7、8個小時,所以相對來說已經(jīng)快了很多,同時沒報錯,一次就過,相當(dāng)順利。編譯組裝好了之后,會在out/gn目錄生成Chromium的可執(zhí)行文件,具體路徑是在:
out/gn/Chromium.app/Contents/MacOS/Chromium
運行這個就可以打開Chromium了:
那么怎么在可視化的XCode里面進(jìn)行debug呢?
2. 在XCode里面Debug
在上面生成build文件的同時,會生成XCode的工程文件:sources.xcodeproj,具體路徑是在:
out/gn/sources.xcodeproj
雙擊這個文件,打開XCode,在上面的菜單欄里面點擊Debug -> AttachToProcess -> Chromium,要先打開Chrome,才能在列表里面看到Chrome的進(jìn)程。然后小試牛刀,打個斷點試試,看會不會跑進(jìn)來:
在左邊的目錄樹,打開chrome/browser/devtools/devtools_protocol.cc這個文件,然后在這個文件的ParseCommand函數(shù)里面打一個斷點,按照字面理解這個函數(shù)應(yīng)該是解析控制臺的命令。打開Chrome的控制臺,輸入一條命令,例如:new Date(),按回車可以看到斷點生效了:
通過觀察變量值,可以看到剛剛敲進(jìn)去的命令。這就說明了我們安裝成功,并且可以通過可視化的方式進(jìn)行調(diào)試。
但是我們要debug頁面渲染過程,Chrome的blink框架使用多進(jìn)程技術(shù),每打開一個tab都會新開一個進(jìn)程,按上面的方式是debug不了構(gòu)建DOM過程的,從Chromium的文檔可以查到,需要在啟動的時候帶上一個參數(shù):
Chromium --renderer-startup-dialog
Chrom的啟動進(jìn)程就會緒塞,并且提示它的渲染進(jìn)程ID:
[7339:775:0102/210122.254760:ERROR:child_process.cc(145)] Renderer (7339) paused waiting for debugger to attach. Send SIGUSR1 to unpause.
7339就是它的渲染進(jìn)程id,在XCode里面點 Debug -> AttachToProcess By Id or Name -> 填入id -> 確定,attach之后,Chrome進(jìn)程就會恢復(fù),然后就可以開始調(diào)試渲染頁面的過程了。
在content/renderer/render_view_impl.cc這個文件的1093行RenderViewImpl::Create函數(shù)里面打個斷點,按照上面的方式,重新啟動Chrome,在命令行帶上某個html文件的路徑,為了打開Chrome的時候就會同時打開這個文件,方便調(diào)試。執(zhí)行完之后就可以看到斷點生效了??梢哉frender_view_impl.cc這個文件是***個具體開始渲染頁面的文件——它會初始化頁面的一些默認(rèn)設(shè)置,如字體大小、默認(rèn)的viewport等,響應(yīng)關(guān)閉頁面、OrientationChange等事件,而在它再往上的層主要是一些負(fù)責(zé)通信的類。
3. Chrome建DOM源碼分析
先畫出構(gòu)建DOM的幾個關(guān)鍵的類的UML圖,如下所示:
***個類HTMLDocumentParser負(fù)責(zé)解析html文本為tokens,一個token就是一個標(biāo)簽文本的序列化,并借助HTMLTreeBuilder對這些tokens分類處理,根據(jù)不同的標(biāo)簽類型、在文檔不同位置,調(diào)用HTMLConstructionSite不同的函數(shù)構(gòu)建DOM樹。而HTMLConstructionSite借助一個工廠類對不同類型的標(biāo)簽創(chuàng)建不同的html元素,并建立起它們的父子兄弟關(guān)系,其中它有一個m_document的成員變量,這個變量就是這棵樹的根結(jié)點,也是js里面的window.document對象。
為作說明,用一個簡單的html文件一步步看這個DOM樹是如何建立起來的:
demo
然后按照上面第2點提到debug的方法,打開Chromium并開始debug:
chromium ~/demo.html --renderer-startup-dialog
我們先來研究一下Chrome的加載和解析機(jī)制
1. 加載機(jī)制
以發(fā)http請求去加載html文本做為我們分析的***步,在此之前的一些初始化就不考慮了。Chrome是在DocumentLoader這個類里面的startLoadingMainResource函數(shù)里去加載url返回的數(shù)據(jù),如訪問一個網(wǎng)站則返回html文本:
FetchRequest fetchRequest(m_request, FetchInitiatorTypeNames::document,
mainResourceLoadOptions);
m_mainResource =
RawResource::fetchMainResource(fetchRequest, fetcher(), m_substituteData);把參數(shù)里的m_request打印出來,在這個函數(shù)里面加一行代碼:
LOG(INFO) << "request url is: " << m_request.url().getString()
并重新編譯Chrome運行,控制臺輸出:
[22731:775:0107/224014.494114:INFO:DocumentLoader.cpp(719)] request url is: “file:///Users/yincheng/demo.html”
可以看到,這個url確實是我們傳進(jìn)的參數(shù)。
發(fā)請求后,每次收到的數(shù)據(jù)塊,會通過Blink封裝的IPC進(jìn)程間通信,觸發(fā)DocumentLoader的dataReceived函數(shù),里面會去調(diào)它commitData函數(shù),開始處理具體業(yè)務(wù)邏輯:
void DocumentLoader::commitData(const char* bytes, size_t length) {
ensureWriter(m_response.mimeType());
if (length)
m_dataReceived = true;
m_writer->addData(bytes, length);
}這個函數(shù)關(guān)鍵行是最2行和第7行,ensureWriter這個函數(shù)會去初始化上面畫的UML圖的解析器HTMLDocumentParser (Parser),并實例化document對象,這些對象都是通過實例m_writer去帶動的。也就是說,writer會去實例化Parser之后,第7行writer傳遞數(shù)據(jù)給Parser去解析。
檢查一下收到的數(shù)據(jù)bytes是什么東西:
可以看到bytes就是請求返回的html文本。
在ensureWriter函數(shù)里面有個判斷:
void DocumentLoader::ensureWriter(const AtomicString& mimeType,
const KURL& overridingURL) {
if (m_writer)
return;
}如果m_writer已經(jīng)初始化過了,則直接返回。也就是說Parser和document只會初始化一次。
在上面的addData函數(shù)里面,會啟動一條線程執(zhí)行Parser的任務(wù):
if (!m_haveBackgroundParser)
startBackgroundParser();并把數(shù)據(jù)傳遞給這條線程進(jìn)行解析,Parser一旦收到數(shù)據(jù)就會序列成tokens,再構(gòu)建DOM樹。
2. 構(gòu)建tokens
這里我們只要關(guān)注序列化后的token是什么東西就好了,為此,寫了一個函數(shù),把tokens的一些關(guān)鍵信息打印出來:
String getTokenInfo(){
String tokenInfo = "";
tokenInfo = "tagName: " + this->m_name + "|type: " + getType() + "|attr:" + getAttributes() + "|text: " + this->m_data;
return tokenInfo;
}打印出來的結(jié)果:
tagName: html |type: DOCTYPE |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: html |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: head |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: \n " tagName: meta |type: startTag |attr:charset=utf-8 |text: " tagName: |type: Character |attr: |text: \n" tagName: head |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: body |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: \n " tagName: div |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: \n " tagName: h1 |type: startTag |attr:class=title |text: " tagName: |type: Character |attr: |text: demo" tagName: h1 |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: \n " tagName: input |type: startTag |attr:value=hello |text: " tagName: |type: Character |attr: |text: \n " tagName: div |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: body |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: html |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: \n" tagName: |type: EndOfFile |attr: |text: "
這些內(nèi)容有標(biāo)簽名、類型、屬性和innerText,標(biāo)簽之間的文本(換行和空白)也會被當(dāng)作一個標(biāo)簽處理。Chrome總共定義了7種標(biāo)簽類型:
enum TokenType {
Uninitialized,
DOCTYPE,
StartTag,
EndTag,
Comment,
Character,
EndOfFile,
};有了一個根結(jié)點document和一些格式化好的tokens,就可以構(gòu)建dom樹了。
3. 構(gòu)建DOM樹
(1)DOM結(jié)點
在研究這個過程之前,先來看一下一個DOM結(jié)點的數(shù)據(jù)結(jié)構(gòu)是怎么樣的。以p標(biāo)簽HTMLParagraphElement為例,畫出它的UML圖,如下所示:
Node是最頂層的父類,它有三個指針,兩個指針分別指向它的前一個結(jié)點和后一個結(jié)點,一個指針指向它的父結(jié)點;
ContainerNode繼承于Node,添加了兩個指針,一個指向***個子元素,另一個指向***一個子元素;
Element又添加了獲取dom結(jié)點屬性、clientWidth、scrollTop等函數(shù)
HTMLElement又繼續(xù)添加了Translate等控制,***一級的子類HTMLParagraphElement只有一個創(chuàng)建的函數(shù),但是它繼承了所有父類的屬性。
需要提到的是每個Node都組合了一個treeScope,這個treeScope記錄了它屬于哪個document(一個頁面可能會嵌入iframe)。
構(gòu)建DOM最關(guān)鍵的步驟應(yīng)該是建立起每個結(jié)點的父子兄弟關(guān)系,即上面提到的成員指針的指向。
到這里我們可以先回答上面提出的***個問題,什么是瀏覽器內(nèi)核
(2)瀏覽器內(nèi)核
瀏覽器內(nèi)核也叫渲染引擎,上面已經(jīng)看到了Chrome是如何實例化一個P標(biāo)簽的,而從firefox的源碼里面P標(biāo)簽的依賴關(guān)系是這樣的:
在代碼實現(xiàn)上和Chrome沒有任何關(guān)系。這就好像W3C出了道題,firefox給了一個解法,取名為Gecko,Safari也給了自己的答案,取名Webkit,Chrome覺得Safari的解法比較好直接拿過來用,又結(jié)合自身的基礎(chǔ)又封裝了一層,取名Blink。由于W3C出的這道題“開放性”比較大,出的時間比較晚,導(dǎo)致各家實現(xiàn)各有花樣。
明白了這點后,繼續(xù)DOM構(gòu)建。下面開始不再說Chrome,叫Webkit或者Blink應(yīng)該更準(zhǔn)確一點
(3)處理開始步驟
Webkit把tokens序列好之后,傳遞給構(gòu)建的線程。在HTMLDocumentParser::processTokenizedChunkFromBackgroundParser的這個函數(shù)里面會做一個循環(huán),把解析好的tokens做一個遍歷,依次調(diào)constructTreeFromCompactHTMLToken進(jìn)行處理。
根據(jù)上面的輸出,最開始處理的***個token是docType的那個:
"tagName: html |type: DOCTYPE |attr: |text: "
在那個函數(shù)里面,首先Parser會調(diào)TreeBuilder的函數(shù):
m_treeBuilder->constructTree(&token);
然后在TreeBuilder里面根據(jù)token的類型做不同的處理:
void HTMLTreeBuilder::processToken(AtomicHTMLToken* token) {
if (token->type() == HTMLToken::Character) {
processCharacter(token);
return;
}
switch (token->type()) {
case HTMLToken::DOCTYPE:
processDoctypeToken(token);
break;
case HTMLToken::StartTag:
processStartTag(token);
break;
case HTMLToken::EndTag:
processEndTag(token);
break;
//othercode
}
}它會對不同類型的結(jié)點做相應(yīng)處理,從上往下依次是文本節(jié)點、doctype節(jié)點、開標(biāo)簽、閉標(biāo)簽。doctype這個結(jié)點比較特殊,單獨作為一種類型處理
(3)DOCType處理
在Parser處理doctype的函數(shù)里面調(diào)了HTMLConstructionSite的插入doctype的函數(shù):
void HTMLTreeBuilder::processDoctypeToken(AtomicHTMLToken* token) {
m_tree.insertDoctype(token);
setInsertionMode(BeforeHTMLMode);
}在這個函數(shù)里面,它會先創(chuàng)建一個doctype的結(jié)點,再創(chuàng)建插dom的task,并設(shè)置文檔類型:
void HTMLConstructionSite::insertDoctype(AtomicHTMLToken* token) {
//const String& publicId = ...
//const String& systemId = ...
DocumentType* doctype =
DocumentType::create(m_document, token->name(), publicId, systemId); //創(chuàng)建DOCType結(jié)點
attachLater(m_attachmentRoot, doctype); //創(chuàng)建插DOM的task
setCompatibilityModeFromDoctype(token->name(), publicId, systemId); //設(shè)置文檔類型
}我們來看一下不同的doctype對文檔類型的設(shè)置有什么影響,如下:
// Check for Quirks Mode.
if (name != "html" ) {
setCompatibilityMode(Document::QuirksMode);
return;
}如果tagName不是html,那么文檔類型將會是怪異模式,以下兩種就會是怪異模式:
而常用的html4寫法:
在源碼里面這個將是有限怪異模式:
// Check for Limited Quirks Mode.
if (!systemId.isEmpty() &&
publicId.startsWith("-//W3C//DTD HTML 4.01 Transitional//",
TextCaseASCIIInsensitive))) {
setCompatibilityMode(Document::LimitedQuirksMode);
return;
}上面的systemId就是”http://www.w3.org/TR/html4/loose.dtd”,它不是空的,所以判斷成立。而如果systemId為空,則它將是怪異模式。如果既不是怪異模式,也不是有限怪異模式,那么它就是標(biāo)準(zhǔn)模式:
// Otherwise we are No Quirks Mode. setCompatibilityMode(Document::NoQuirksMode);
常用的html5的寫法就是標(biāo)準(zhǔn)模式,如果連DOCType聲明也沒有呢?那么會默認(rèn)設(shè)置為怪異模式:
void HTMLConstructionSite::setDefaultCompatibilityMode() {
setCompatibilityMode(Document::QuirksMode);
}這些模式有什么區(qū)別,從源碼注釋可窺探一二:
// There are three possible compatibility modes: // Quirks - quirks mode emulates WinIE and NS4. CSS parsing is also relaxed in // this mode, e.g., unit types can be omitted from numbers. // Limited Quirks - This mode is identical to no-quirks mode except for its // treatment of line-height in the inline box model. // No Quirks - no quirks apply. Web pages will obey the specifications to the // letter.
大意是說,怪異模式會模擬IE,同時CSS解析會比較寬松,例如數(shù)字單位可以省略,而有限怪異模式和標(biāo)準(zhǔn)模式的唯一區(qū)別在于在于對inline元素的行高處理不一樣。標(biāo)準(zhǔn)模式將會讓頁面遵守文檔規(guī)定。
怪異模式下的input和textarea的默認(rèn)盒模型將會變成border-box:
標(biāo)準(zhǔn)模式下的文檔高度是實際內(nèi)容的高度:
而在怪異模式下的文檔高度是窗口可視域的高度:
在有限怪異模式下,div里面的圖片下方不會留空白,如下圖左所示;而在標(biāo)準(zhǔn)模式下div下方會留點空白,如下圖右所示:
這個空白是div的行高撐起來的,當(dāng)把div的行高設(shè)置成0的時候,就沒有下面的空白了。在怪異模和有限怪異模式下,為了計算行內(nèi)子元素的最小高度,一個塊級元素的行高必須被忽略。
這里的敘述雖然跟解讀源碼沒有直接的關(guān)系(我們還沒解讀到CSS處理),但是很有必要提一下。
接下來我們開始正式說明DOM構(gòu)建
(4)開標(biāo)簽處理
下一個遇到的開標(biāo)簽是標(biāo)簽,處理這個標(biāo)簽的任務(wù)應(yīng)該是實例化一個HTMLHtmlElement元素,然后把它的父元素指向document。Webkit源碼里面使用了一個m_attachmentRoot的變量記錄attach的根結(jié)點,初始化HTMLConstructionSite也會初始化這個變量,值為document:
HTMLConstructionSite::HTMLConstructionSite(
Document& document)
: m_document(&document),
m_attachmentRoot(document)) {
}所以html結(jié)點的父結(jié)點就是document,實際的操作過程是這樣的:
void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomicHTMLToken* token) {
HTMLHtmlElement* element = HTMLHtmlElement::create(*m_document);
attachLater(m_attachmentRoot, element);
m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element, token));
executeQueuedTasks();
}第二行先創(chuàng)建一個html結(jié)點,第三行把它加到一個任務(wù)隊列里面,傳遞兩個參數(shù),***個參數(shù)是父結(jié)點,第二個參數(shù)是當(dāng)前結(jié)點,第五行執(zhí)行隊列里面的任務(wù)。代碼第四行會把它壓到一個棧里面,這個棧存放了未遇到閉標(biāo)簽的所有開標(biāo)簽。
第三行attachLater是如何建立一個task的:
void HTMLConstructionSite::attachLater(ContainerNode* parent,
Node* child,
bool selfClosing) {
HTMLConstructionSiteTask task(HTMLConstructionSiteTask::Insert);
task.parent = parent;
task.child = child;
task.selfClosing = selfClosing;
// Add as a sibling of the parent if we have reached the maximum depth
// allowed.
if (m_openElements.stackDepth() > maximumHTMLParserDOMTreeDepth &&
task.parent->parentNode())
task.parent = task.parent->parentNode();
queueTask(task);
}代碼邏輯比較簡單,比較有趣的是發(fā)現(xiàn)DOM樹有一個***的深度:maximumHTMLParserDOMTreeDepth,超過這個***深度就會把它子元素當(dāng)作父無素的同級節(jié)點,這個***值是多少呢?512:
static const unsigned maximumHTMLParserDOMTreeDepth = 512;
我們重點關(guān)注executeQueuedTasks干了些什么,它會根據(jù)task的類型執(zhí)行不同的操作,由于本次是insert的,它會去執(zhí)行一個插入的函數(shù):
void ContainerNode::parserAppendChild(Node* newChild) {
if (!checkParserAcceptChild(*newChild))
return;
AdoptAndAppendChild()(*this, *newChild, nullptr);
}
notifyNodeInserted(*newChild, ChildrenChangeSourceParser);
}在插入里面它會先去檢查父元素是否支持子元素,如果不支持,則直接返回,就像video標(biāo)簽不支持子元素。然后再去調(diào)具體的插入:
void ContainerNode::appendChildCommon(Node& child) {
child.setParentOrShadowHostNode(this);
if (m_lastChild) {
child.setPreviousSibling(m_lastChild);
m_lastChild->setNextSibling(&child);
} else {
setFirstChild(&child);
}
setLastChild(&child);
}上面代碼第二行,設(shè)置子元素的父結(jié)點,也就是會把html結(jié)點的父結(jié)點指向document,然后如果沒有l(wèi)astChild,會將這個子元素作為firstChild,由于上面已經(jīng)有一個docype的子結(jié)點了,所以已經(jīng)有l(wèi)astChild了,因此會把這個子元素的previousSibling指向老的lastChild,老的lastChild的nexSibling指向它。***倒數(shù)第二行再把子元素設(shè)置為當(dāng)前ContainerNode(即document)的lastChild。這樣就建立起了html結(jié)點的父子兄弟關(guān)系。
可以看到,借助上一次的m_lastChild建立起了兄弟關(guān)系。
這個時候你可能會有一個問題,為什么要用一個task隊列存放將要插入的結(jié)點呢,而不是直接插入呢?一個原因是放到task里面方便統(tǒng)一處理,并且有些task可能不能立即執(zhí)行,要先存起來。不過在我們這個案例里面都是存完后下一步就執(zhí)行了。
當(dāng)遇到head標(biāo)簽的token時,也是先創(chuàng)建一個head結(jié)點,然后再創(chuàng)建一個task,插到隊列里面:
void HTMLConstructionSite::insertHTMLHeadElement(AtomicHTMLToken* token) {
m_head = HTMLStackItem::create(createHTMLElement(token), token);
attachLater(currentNode(), m_head->element());
m_openElements.pushHTMLHeadElement(m_head);
}attachLater傳參的***個參數(shù)為父結(jié)點,這個currentNode為開標(biāo)簽棧里面的最頂?shù)脑兀?/p>
ContainerNode* currentNode() const {
return m_openElements.topNode();
}我們剛剛把html元素壓了進(jìn)去,則棧頂元素為html元素,所以head的父結(jié)點就為html。所以每當(dāng)遇到一個開標(biāo)簽時,就把它壓起來,下一次再遇到一個開標(biāo)簽時,它的父元素就是上一個開標(biāo)簽。
所以,初步可以看到,借助一個棧建立起了父子關(guān)系。
而當(dāng)遇到一個閉標(biāo)簽?zāi)兀?/p>
(5)處理閉標(biāo)簽
當(dāng)遇到一個閉標(biāo)簽時,會把棧里面的元素一直pop出來,直到pop到***個和它標(biāo)簽名字一樣的:
m_tree.openElements()->popUntilPopped(token->name());
我們***個遇到的是閉標(biāo)簽是head標(biāo)簽,它會把開的head標(biāo)簽pop出來,棧里面就剩下html元素了,所以當(dāng)再遇到body時,html元素就是body的父元素了。
這個是棧的一個典型應(yīng)用。
以下面的html為例來研究壓棧和出棧的過程:
hello
demo
把push和pop打印出來是這樣的:
push "HTML" m_stackDepth = 1 push "HEAD" m_stackDepth = 2 pop "HEAD" m_stackDepth = 1 push "BODY" m_stackDepth = 2 push "DIV" m_stackDepth = 3 push "P" m_stackDepth = 4 push "B" m_stackDepth = 5 pop "B" m_stackDepth = 4 pop "P" m_stackDepth = 3 push "P" m_stackDepth = 4 pop "P" m_stackDepth = 3 pop "DIV" m_stackDepth = 2 "tagName: body |type: EndTag |attr: |text: " "tagName: html |type: EndTag |attr: |text: "
這個過程確實和上面的描述一致,遇到一個閉標(biāo)簽就把一次的開標(biāo)簽pop出來。
并且可以發(fā)現(xiàn)遇到body閉標(biāo)簽后,并不會把body給pop出來,因為如果body閉標(biāo)簽后面又再寫了標(biāo)簽的話,就會自動當(dāng)成body的子元素。
假設(shè)上面的b標(biāo)簽的閉標(biāo)簽忘記寫了,又會發(fā)生什么:
hello
打印出來的結(jié)果是這樣的:
push "P" m_stackDepth = 4 push "B" m_stackDepth = 5 "tagName: p |type: EndTag |attr: |text: " pop "B" m_stackDepth = 4 pop "P" m_stackDepth = 3 push "B" m_stackDepth = 4 push "P" m_stackDepth = 5 pop "P" m_stackDepth = 4 pop "B" m_stackDepth = 3 pop "DIV" m_stackDepth = 2 push "B" m_stackDepth = 3
同樣地,在上面第3行,遇到P閉標(biāo)簽時,會把所有的開標(biāo)簽pop出來,直到遇到P標(biāo)簽。不同的是后續(xù)的過程中會不斷地插入b標(biāo)簽,***渲染的頁面結(jié)構(gòu):
因為b等帶有格式化的標(biāo)簽會特殊處理,遇到一個開標(biāo)簽時會它們放到一個列表里面:
// a, b, big, code, em, font, i, nobr, s, small, strike, strong, tt, and u. m_activeFormattingElements.append(currentElementRecord()->stackItem());
遇到一個閉標(biāo)簽時,又會從這個列表里面刪掉。每處理一個新標(biāo)簽時就會進(jìn)行檢查和這個列表和棧里的開標(biāo)簽是否對應(yīng),如果不對應(yīng)則會reconstruct:重新插入一個開標(biāo)簽。因此b就不斷地被重新插入,直到遇到下一個b的閉標(biāo)簽為止。
如果上面少寫的是一個span,那么渲染之后的結(jié)果是正常的:
而對于文本節(jié)點是實例化了Text的對象,這里不再展開討論。
(6)自定義標(biāo)簽的處理
在瀏覽器里面可以看到,自定義標(biāo)簽?zāi)J(rèn)不會有任何的樣式,并且它默認(rèn)是一個行內(nèi)元素:
初步觀察它和span標(biāo)簽的表現(xiàn)是一樣的:
在blink的源碼里面,不認(rèn)識的標(biāo)簽?zāi)J(rèn)會被實例化成一個HTMLUnknownElement,這個類對外提供了一個create函數(shù),這和HTMLSpanElement是一樣的,只有一個create函數(shù),并且大家都是繼承于HTMLElement。并且創(chuàng)建span標(biāo)簽的時候和unknown一樣,并沒有做特殊處理,直接調(diào)的create。所以從本質(zhì)上來說,可以把自定義的標(biāo)簽當(dāng)作一個span看待。然后你可以再設(shè)置display: block改成塊級元素之類的。
但是你可以用js定義一個自定義標(biāo)簽,定義它的屬性等,Webkit會去讀它的定義:
// "4. Let definition be the result of looking up a custom element ..." etc.
CustomElementDefinition* definition =
m_isParsingFragment ? nullptr
: lookUpCustomElementDefinition(document, token);例如給自定義標(biāo)簽創(chuàng)建一個原生屬性:
NO. 2 high school
上面定義了一個country,為了可以直接獲取這個屬性:
console.log(document.getElementsByTagName("high-school")[0].country);注冊一個自定義標(biāo)簽:
window.customElements.define("high-school", HighSchoolElement);這個HighSchoolElement繼承于HTMLElement:
class HighSchoolElement extends HTMLElement{
constructor(){
super();
this._country = null;
}
get country(){
return this._country;
}
set country(country){
this.setAttribute("country", _country);
}
static get observedAttributes() {
return ["country"];
}
attributeChangedCallback(name, oldValue, newValue) {
this._country = newValue;
this._updateRender(name, oldValue, newValue);
}
_updateRender(name, oldValue, newValue){
console.log(name + " change from " + oldValue + " " + newValue);
}
}就可以直接取到contry這個屬性,而不用通過getAttribute的函數(shù),并且可以在屬性發(fā)生變化時更新元素的渲染,改變color等。詳見Custom Elements – W3C.
通過這種方式創(chuàng)建的,它就不是一個HTMLUnknownElement了。blink通過V8引擎把js的構(gòu)造函數(shù)轉(zhuǎn)化成C++的函數(shù),實例化一個HTMLElement的對象。
***再來看查DOM的過程
4. 查DOM過程
(1)按ID查找
在頁面添加一個script:
Chrome的V8引擎把js代碼層層轉(zhuǎn)化,***會調(diào):
DocumentV8Internal::getElementByIdMethodForMainWorld(info);
而這個函數(shù)又會調(diào)TreeScope的getElementById的函數(shù),TreeScope存儲了一個m_map的哈希map,這個map以標(biāo)簽id字符串作為key值,Element為value值,我們可以把這個map打印出來:
Map::iterator it = m_map.begin();
while(it != m_map.end()){
LOG(INFO) << it->key << " " << it->value->element->tagName();
++it;
}html結(jié)構(gòu)是這樣的:
yin
20
打印出來的結(jié)果為:
"id-age" "P" "id-sex" "P" "id-name" "P" "id-yin" "DIV"
可以看到, 這個m_map把頁面所有有id的標(biāo)簽都存了進(jìn)來。由于map的查找時間復(fù)雜度為O(1),所以使用ID選擇器可以說是最快的。
再來看一下類選擇器:
(2)類選擇器
js如下:
var users = document.getElementsByClassName("user");
users.length;在執(zhí)行***行的時候,Webkit返回了一個ClassCollection的列表:
return new ClassCollection(rootNode, classNames);
而這個列表并不是去查DOM獲取的,它只是記錄了className作為標(biāo)志。這與我們的認(rèn)知是一致的,這種HTMLCollection的數(shù)據(jù)結(jié)構(gòu)都是在使用的時候才去查DOM,所以在上面第二行去獲取它的length,就會觸發(fā)它的查DOM,在nodeCount這個函數(shù)里面執(zhí)行:
NodeType* currentNode = collection.traverseToFirst();
unsigned currentIndex = 0;
while (currentNode) {
m_cachedList.push_back(currentNode);
currentNode = collection.traverseForwardToOffset(
currentIndex + 1, *currentNode, currentIndex);
}***行先獲取符合collection條件的***個結(jié)點,然后不斷獲取下一個符合條件的結(jié)點,直到null,并把它存到一個cachedList里面,下次再獲取這個collection的東西時便不用再重復(fù)查DOM,只要cached仍然是有效的:
if (this->isCachedNodeCountValid())
return this->cachedNodeCount();怎么樣找到有效的節(jié)點呢:
ElementType* element = Traversal::firstWithin(current); while (element && !isMatch(*element)) element = Traversal ::next(*element, ¤t, isMatch); return element;
***行先獲取***個節(jié)點,如果它沒有match,則繼續(xù)next,直到找到符合條件或者空為止。我們的重點在于,它是怎么遍歷的,如何next獲取下一個節(jié)點,核心代碼:
if (current.hasChildren())
return current.firstChild();
if (current == stayWithin)
return 0;
if (current.nextSibling())
return current.nextSibling();
return nextAncestorSibling(current, stayWithin);***行先判斷當(dāng)前節(jié)點有沒有子元素,如果有的話返回它的***個子元素,如果當(dāng)前節(jié)點沒有子元素,并且這個節(jié)點就是開始找的根元素(用document.getElement*,則為document),則說明沒有下一個元素了,直接返回0/null。如果這個節(jié)點不是根元素了(例如已經(jīng)到了子元素這一層),那么看它有沒有相鄰元素,如果有則返回下一個相鄰元素,如果相鄰無素也沒有了,由于它是一個葉子結(jié)點(沒有子元素),說明它已經(jīng)到了最深的一層,并且是當(dāng)前層的***一個葉子結(jié)點,那么就返回它的父元素的下一個相鄰節(jié)點,如果這個也沒有了,則返回null,查找結(jié)束。可以看出這是一個深度優(yōu)先的查找。
(3)querySelector
a)先來看下selector為一個id時發(fā)生了什么:
document.querySelector("#id-name");它會調(diào)ContainerNode的querySelecotr函數(shù):
SelectorQuery* selectorQuery = document().selectorQueryCache().add(
selectors, document(), exceptionState);
return selectorQuery->queryFirst(*this);先把輸入的selector字符串序列化成一個selectorQuery,然后再queryFirst,通過打斷點可以發(fā)現(xiàn),它***會調(diào)的TreeScope的getElementById:
rootNode.treeScope().getElementById(idToMatch);
b)如果selector為一個class:
document.querySelector(".user");它會從document開始遍歷:
for (Element& element : ElementTraversal::descendantsOf(rootNode)) {
if (element.hasClass() && element.classNames().contains(className)) {
SelectorQueryTrait::appendElement(output, element);
if (SelectorQueryTrait::shouldOnlyMatchFirstElement)
return;
}
}我們重點查看它是怎么遍歷,即***行的for循環(huán)。表面上看它好像把所有的元素取出來然后做個循環(huán),其實不然,它是重載++操作符:
void operator++() { m_current = TraversalNext::next(*m_current, m_root); }只要我們看下next是怎么操作的就可以得知它是怎么遍歷,而這個next跟上面的講解class時是調(diào)的同一個next。不一樣的是match條件判斷是:有className,并且className列表里面包含這個class,如上面代碼第二行。
c)復(fù)雜選擇器
例如寫兩個class:
document.querySelector(".user .important");最終也會轉(zhuǎn)成一個遍歷,只是判斷是否match的條件不一樣:
for (Element& element : ElementTraversal::descendantsOf(*traverseRoot)) {
if (selectorMatches(selector, element, rootNode)) {
SelectorQueryTrait::appendElement(output, element);
if (SelectorQueryTrait::shouldOnlyMatchFirstElement)
return;
}
}怎么判斷是否match比較復(fù)雜,這里不再展開討論。
同時在源碼可以看到,如果是怪異模式,會調(diào)一個executeSlow的查詢,并且判斷match條件也不一樣。不過遍歷是一樣的。
查看源碼確實是一件很費時費力的工作,但是通過一番探索,能夠了解瀏覽器的一些內(nèi)在機(jī)制,至少已經(jīng)可以回答上面提出來的幾個問題。同時知道了Webkit/Blink借助一個棧,結(jié)合開閉標(biāo)簽,一步步構(gòu)建DOM樹,并對DOCType的標(biāo)簽、自定義標(biāo)簽的處理有了一定的了解。***又討論了查DOM的幾種情況,明白了查找的過程。
通過上面的分析,對頁面渲染的***步構(gòu)建DOM應(yīng)該會有一個基礎(chǔ)的了解。
文章標(biāo)題:從Chrome源碼看瀏覽器如何構(gòu)建DOM樹
本文路徑:http://www.5511xx.com/article/dhijoie.html


咨詢
建站咨詢

