自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從Chrome源碼看瀏覽器如何構(gòu)建DOM樹

開發(fā) 開發(fā)工具 瀏覽器
查看源碼確實(shí)是一件很費(fèi)時(shí)費(fèi)力的工作,但是通過一番探索,能夠了解瀏覽器的一些內(nèi)在機(jī)制,至少已經(jīng)可以回答上面提出來的幾個(gè)問題。同時(shí)知道了Webkit/Blink借助一個(gè)棧,結(jié)合開閉標(biāo)簽,一步步構(gòu)建DOM樹,并對(duì)DOCType的標(biāo)簽、自定義標(biāo)簽的處理有了一定的了解。最后又討論了查DOM的幾種情況,明白了查找的過程。

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

  1. IE用的是Trident內(nèi)核,Safari用的是Webkit,Chrome用的是Blink,到底什么是內(nèi)核,它們的區(qū)別是什么?
  2. 如果沒有聲明<!DOCTYPE html>會(huì)造成什么影響?
  3. 瀏覽器如何處理自定義的標(biāo)簽,如寫一個(gè)<data></data>?
  4. 查DOM的過程是怎么樣的?

先說一下,怎么安裝一個(gè)可以debug的Chrome

1. 從源碼安裝Chrome

為了可以打斷點(diǎn)debug,必須得從頭編譯(編譯的時(shí)候帶上debug參數(shù))。所以要下載源碼,Chrome把***的代碼更新到了Chromium的工程,是完全開源的,你可以把它整一個(gè)git工程下載下來。Chromium的下載安裝可參考它的文檔, 這里把一些關(guān)鍵點(diǎn)說一下,以Mac為例。你需要先下載它的安裝腳本工具,然后下載源碼:

fetch chromium --no-history

–no-history的作用是不把整個(gè)git工程下載下來,那個(gè)實(shí)在是太大了。或者是直接執(zhí)行g(shù)it clone:

git clone https://chromium.googlesource.com/chromium/src

這個(gè)就是整一個(gè)git工程,下載下來有6.48GB(那時(shí))。博主就是用的這樣的方式,如果下載到***提示出錯(cuò)了:

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,因?yàn)閷?shí)在太大、太耗時(shí)了。

下載好之后生成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個(gè)小時(shí),firfox的源碼需要編譯7、8個(gè)小時(shí),所以相對(duì)來說已經(jīng)快了很多,同時(shí)沒報(bào)錯(cuò),一次就過,相當(dāng)順利。編譯組裝好了之后,會(huì)在out/gn目錄生成Chromium的可執(zhí)行文件,具體路徑是在:

out/gn/Chromium.app/Contents/MacOS/Chromium

運(yùn)行這個(gè)就可以打開Chromium了:

那么怎么在可視化的XCode里面進(jìn)行debug呢?

2. 在XCode里面Debug

在上面生成build文件的同時(shí),會(huì)生成XCode的工程文件:sources.xcodeproj,具體路徑是在:

out/gn/sources.xcodeproj

雙擊這個(gè)文件,打開XCode,在上面的菜單欄里面點(diǎn)擊Debug -> AttachToProcess -> Chromium,要先打開Chrome,才能在列表里面看到Chrome的進(jìn)程。然后小試牛刀,打個(gè)斷點(diǎn)試試,看會(huì)不會(huì)跑進(jìn)來:

在左邊的目錄樹,打開chrome/browser/devtools/devtools_protocol.cc這個(gè)文件,然后在這個(gè)文件的ParseCommand函數(shù)里面打一個(gè)斷點(diǎn),按照字面理解這個(gè)函數(shù)應(yīng)該是解析控制臺(tái)的命令。打開Chrome的控制臺(tái),輸入一條命令,例如:new Date(),按回車可以看到斷點(diǎn)生效了:

通過觀察變量值,可以看到剛剛敲進(jìn)去的命令。這就說明了我們安裝成功,并且可以通過可視化的方式進(jìn)行調(diào)試。

但是我們要debug頁面渲染過程,Chrome的blink框架使用多進(jìn)程技術(shù),每打開一個(gè)tab都會(huì)新開一個(gè)進(jìn)程,按上面的方式是debug不了構(gòu)建DOM過程的,從Chromium的文檔可以查到,需要在啟動(dòng)的時(shí)候帶上一個(gè)參數(shù):

Chromium --renderer-startup-dialog

Chrom的啟動(dòng)進(jìn)程就會(huì)緒塞,并且提示它的渲染進(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里面點(diǎn) Debug -> AttachToProcess By Id or Name -> 填入id -> 確定,attach之后,Chrome進(jìn)程就會(huì)恢復(fù),然后就可以開始調(diào)試渲染頁面的過程了。

在content/renderer/render_view_impl.cc這個(gè)文件的1093行RenderViewImpl::Create函數(shù)里面打個(gè)斷點(diǎn),按照上面的方式,重新啟動(dòng)Chrome,在命令行帶上某個(gè)html文件的路徑,為了打開Chrome的時(shí)候就會(huì)同時(shí)打開這個(gè)文件,方便調(diào)試。執(zhí)行完之后就可以看到斷點(diǎn)生效了。可以說render_view_impl.cc這個(gè)文件是***個(gè)具體開始渲染頁面的文件——它會(huì)初始化頁面的一些默認(rèn)設(shè)置,如字體大小、默認(rèn)的viewport等,響應(yīng)關(guān)閉頁面、OrientationChange等事件,而在它再往上的層主要是一些負(fù)責(zé)通信的類。

3. Chrome建DOM源碼分析

先畫出構(gòu)建DOM的幾個(gè)關(guān)鍵的類的UML圖,如下所示:

***個(gè)類HTMLDocumentParser負(fù)責(zé)解析html文本為tokens,一個(gè)token就是一個(gè)標(biāo)簽文本的序列化,并借助HTMLTreeBuilder對(duì)這些tokens分類處理,根據(jù)不同的標(biāo)簽類型、在文檔不同位置,調(diào)用HTMLConstructionSite不同的函數(shù)構(gòu)建DOM樹。而HTMLConstructionSite借助一個(gè)工廠類對(duì)不同類型的標(biāo)簽創(chuàng)建不同的html元素,并建立起它們的父子兄弟關(guān)系,其中它有一個(gè)m_document的成員變量,這個(gè)變量就是這棵樹的根結(jié)點(diǎn),也是js里面的window.document對(duì)象。

為作說明,用一個(gè)簡單的html文件一步步看這個(gè)DOM樹是如何建立起來的:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
    <div>
        <h1 class="title">demo</h1>
        <input value="hello">
    </div>
</body>
</html>

然后按照上面第2點(diǎn)提到debug的方法,打開Chromium并開始debug:

chromium ~/demo.html --renderer-startup-dialog

我們先來研究一下Chrome的加載和解析機(jī)制

1. 加載機(jī)制

以發(fā)http請(qǐng)求去加載html文本做為我們分析的***步,在此之前的一些初始化就不考慮了。Chrome是在DocumentLoader這個(gè)類里面的startLoadingMainResource函數(shù)里去加載url返回的數(shù)據(jù),如訪問一個(gè)網(wǎng)站則返回html文本:

  FetchRequest fetchRequest(m_request, FetchInitiatorTypeNames::document,
                            mainResourceLoadOptions);
  m_mainResource =
      RawResource::fetchMainResource(fetchRequest, fetcher(), m_substituteData);

把參數(shù)里的m_request打印出來,在這個(gè)函數(shù)里面加一行代碼:

LOG(INFO) << "request url is: " << m_request.url().getString()

并重新編譯Chrome運(yùn)行,控制臺(tái)輸出:

[22731:775:0107/224014.494114:INFO:DocumentLoader.cpp(719)] request url is: “file:///Users/yincheng/demo.html”

可以看到,這個(gè)url確實(shí)是我們傳進(jìn)的參數(shù)。

發(fā)請(qǐng)求后,每次收到的數(shù)據(jù)塊,會(huì)通過Blink封裝的IPC進(jìn)程間通信,觸發(fā)DocumentLoader的dataReceived函數(shù),里面會(huì)去調(diào)它c(diǎn)ommitData函數(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);
}

這個(gè)函數(shù)關(guān)鍵行是最2行和第7行,ensureWriter這個(gè)函數(shù)會(huì)去初始化上面畫的UML圖的解析器HTMLDocumentParser (Parser),并實(shí)例化document對(duì)象,這些對(duì)象都是通過實(shí)例m_writer去帶動(dòng)的。也就是說,writer會(huì)去實(shí)例化Parser之后,第7行writer傳遞數(shù)據(jù)給Parser去解析。

檢查一下收到的數(shù)據(jù)bytes是什么東西:

可以看到bytes就是請(qǐng)求返回的html文本。

在ensureWriter函數(shù)里面有個(gè)判斷:

void DocumentLoader::ensureWriter(const AtomicString& mimeType,
                                  const KURL& overridingURL) {
  if (m_writer)
    return;

}

如果m_writer已經(jīng)初始化過了,則直接返回。也就是說Parser和document只會(huì)初始化一次。

在上面的addData函數(shù)里面,會(huì)啟動(dòng)一條線程執(zhí)行Parser的任務(wù):

if (!m_haveBackgroundParser)
      startBackgroundParser();

并把數(shù)據(jù)傳遞給這條線程進(jìn)行解析,Parser一旦收到數(shù)據(jù)就會(huì)序列成tokens,再構(gòu)建DOM樹。

2. 構(gòu)建tokens

這里我們只要關(guān)注序列化后的token是什么東西就好了,為此,寫了一個(gè)函數(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)簽之間的文本(換行和空白)也會(huì)被當(dāng)作一個(gè)標(biāo)簽處理。Chrome總共定義了7種標(biāo)簽類型:

  enum TokenType {
    Uninitialized,
    DOCTYPE,
    StartTag,
    EndTag,
    Comment,
    Character,
    EndOfFile,
  };

有了一個(gè)根結(jié)點(diǎn)document和一些格式化好的tokens,就可以構(gòu)建dom樹了。

3. 構(gòu)建DOM樹

(1)DOM結(jié)點(diǎn)

在研究這個(gè)過程之前,先來看一下一個(gè)DOM結(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu)是怎么樣的。以p標(biāo)簽HTMLParagraphElement為例,畫出它的UML圖,如下所示:

Node是最頂層的父類,它有三個(gè)指針,兩個(gè)指針分別指向它的前一個(gè)結(jié)點(diǎn)和后一個(gè)結(jié)點(diǎn),一個(gè)指針指向它的父結(jié)點(diǎn);

ContainerNode繼承于Node,添加了兩個(gè)指針,一個(gè)指向***個(gè)子元素,另一個(gè)指向***一個(gè)子元素;

Element又添加了獲取dom結(jié)點(diǎn)屬性、clientWidth、scrollTop等函數(shù)

HTMLElement又繼續(xù)添加了Translate等控制,***一級(jí)的子類HTMLParagraphElement只有一個(gè)創(chuàng)建的函數(shù),但是它繼承了所有父類的屬性。

需要提到的是每個(gè)Node都組合了一個(gè)treeScope,這個(gè)treeScope記錄了它屬于哪個(gè)document(一個(gè)頁面可能會(huì)嵌入iframe)。

構(gòu)建DOM最關(guān)鍵的步驟應(yīng)該是建立起每個(gè)結(jié)點(diǎn)的父子兄弟關(guān)系,即上面提到的成員指針的指向。

到這里我們可以先回答上面提出的***個(gè)問題,什么是瀏覽器內(nèi)核

(2)瀏覽器內(nèi)核

瀏覽器內(nèi)核也叫渲染引擎,上面已經(jīng)看到了Chrome是如何實(shí)例化一個(gè)P標(biāo)簽的,而從firefox的源碼里面P標(biāo)簽的依賴關(guān)系是這樣的:

在代碼實(shí)現(xiàn)上和Chrome沒有任何關(guān)系。這就好像W3C出了道題,firefox給了一個(gè)解法,取名為Gecko,Safari也給了自己的答案,取名Webkit,Chrome覺得Safari的解法比較好直接拿過來用,又結(jié)合自身的基礎(chǔ)又封裝了一層,取名Blink。由于W3C出的這道題“開放性”比較大,出的時(shí)間比較晚,導(dǎo)致各家實(shí)現(xiàn)各有花樣。

明白了這點(diǎn)后,繼續(xù)DOM構(gòu)建。下面開始不再說Chrome,叫Webkit或者Blink應(yīng)該更準(zhǔn)確一點(diǎn)

(3)處理開始步驟

Webkit把tokens序列好之后,傳遞給構(gòu)建的線程。在HTMLDocumentParser::processTokenizedChunkFromBackgroundParser的這個(gè)函數(shù)里面會(huì)做一個(gè)循環(huán),把解析好的tokens做一個(gè)遍歷,依次調(diào)constructTreeFromCompactHTMLToken進(jìn)行處理。

根據(jù)上面的輸出,最開始處理的***個(gè)token是docType的那個(gè):

"tagName: html  |type: DOCTYPE   |attr:              |text: "

在那個(gè)函數(shù)里面,首先Parser會(huì)調(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
  }
}

它會(huì)對(duì)不同類型的結(jié)點(diǎn)做相應(yīng)處理,從上往下依次是文本節(jié)點(diǎn)、doctype節(jié)點(diǎn)、開標(biāo)簽、閉標(biāo)簽。doctype這個(gè)結(jié)點(diǎn)比較特殊,單獨(dú)作為一種類型處理

(3)DOCType處理

在Parser處理doctype的函數(shù)里面調(diào)了HTMLConstructionSite的插入doctype的函數(shù):

void HTMLTreeBuilder::processDoctypeToken(AtomicHTMLToken* token) {
    m_tree.insertDoctype(token);
    setInsertionMode(BeforeHTMLMode);
}

在這個(gè)函數(shù)里面,它會(huì)先創(chuàng)建一個(gè)doctype的結(jié)點(diǎn),再創(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é)點(diǎn)
  attachLater(m_attachmentRoot, doctype);  //創(chuàng)建插DOM的task
  setCompatibilityModeFromDoctype(token->name(), publicId, systemId); //設(shè)置文檔類型
}

我們來看一下不同的doctype對(duì)文檔類型的設(shè)置有什么影響,如下:

  // Check for Quirks Mode.
  if (name != "html" ) {
    setCompatibilityMode(Document::QuirksMode);
    return;
  }

如果tagName不是html,那么文檔類型將會(huì)是怪異模式,以下兩種就會(huì)是怪異模式:

<!DOCType svg>
<!DOCType math>

而常用的html4寫法:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

在源碼里面這個(gè)將是有限怪異模式:

  // 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聲明也沒有呢?那么會(huì)默認(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.

大意是說,怪異模式會(huì)模擬IE,同時(shí)CSS解析會(huì)比較寬松,例如數(shù)字單位可以省略,而有限怪異模式和標(biāo)準(zhǔn)模式的唯一區(qū)別在于在于對(duì)inline元素的行高處理不一樣。標(biāo)準(zhǔn)模式將會(huì)讓頁面遵守文檔規(guī)定。

怪異模式下的input和textarea的默認(rèn)盒模型將會(huì)變成border-box:

標(biāo)準(zhǔn)模式下的文檔高度是實(shí)際內(nèi)容的高度:

而在怪異模式下的文檔高度是窗口可視域的高度:

在有限怪異模式下,div里面的圖片下方不會(huì)留空白,如下圖左所示;而在標(biāo)準(zhǔn)模式下div下方會(huì)留點(diǎn)空白,如下圖右所示:

<div><img src="test.jpg" style="height:100px"></div>

這個(gè)空白是div的行高撐起來的,當(dāng)把div的行高設(shè)置成0的時(shí)候,就沒有下面的空白了。在怪異模和有限怪異模式下,為了計(jì)算行內(nèi)子元素的最小高度,一個(gè)塊級(jí)元素的行高必須被忽略。

這里的敘述雖然跟解讀源碼沒有直接的關(guān)系(我們還沒解讀到CSS處理),但是很有必要提一下。

接下來我們開始正式說明DOM構(gòu)建

(4)開標(biāo)簽處理

下一個(gè)遇到的開標(biāo)簽是<html>標(biāo)簽,處理這個(gè)標(biāo)簽的任務(wù)應(yīng)該是實(shí)例化一個(gè)HTMLHtmlElement元素,然后把它的父元素指向document。Webkit源碼里面使用了一個(gè)m_attachmentRoot的變量記錄attach的根結(jié)點(diǎn),初始化HTMLConstructionSite也會(huì)初始化這個(gè)變量,值為document:

HTMLConstructionSite::HTMLConstructionSite(
    Document& document)
    : m_document(&document),
      m_attachmentRoot(document)) {
}

所以html結(jié)點(diǎn)的父結(jié)點(diǎn)就是document,實(shí)際的操作過程是這樣的:

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)建一個(gè)html結(jié)點(diǎn),第三行把它加到一個(gè)任務(wù)隊(duì)列里面,傳遞兩個(gè)參數(shù),***個(gè)參數(shù)是父結(jié)點(diǎn),第二個(gè)參數(shù)是當(dāng)前結(jié)點(diǎn),第五行執(zhí)行隊(duì)列里面的任務(wù)。代碼第四行會(huì)把它壓到一個(gè)棧里面,這個(gè)棧存放了未遇到閉標(biāo)簽的所有開標(biāo)簽。

第三行attachLater是如何建立一個(gè)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樹有一個(gè)***的深度:maximumHTMLParserDOMTreeDepth,超過這個(gè)***深度就會(huì)把它子元素當(dāng)作父無素的同級(jí)節(jié)點(diǎn),這個(gè)***值是多少呢?512:

static const unsigned maximumHTMLParserDOMTreeDepth = 512;

我們重點(diǎn)關(guān)注executeQueuedTasks干了些什么,它會(huì)根據(jù)task的類型執(zhí)行不同的操作,由于本次是insert的,它會(huì)去執(zhí)行一個(gè)插入的函數(shù):

void ContainerNode::parserAppendChild(Node* newChild) {
  if (!checkParserAcceptChild(*newChild))
    return;
    AdoptAndAppendChild()(*this, *newChild, nullptr);
  }
  notifyNodeInserted(*newChild, ChildrenChangeSourceParser);
}

在插入里面它會(huì)先去檢查父元素是否支持子元素,如果不支持,則直接返回,就像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é)點(diǎn),也就是會(huì)把html結(jié)點(diǎn)的父結(jié)點(diǎn)指向document,然后如果沒有l(wèi)astChild,會(huì)將這個(gè)子元素作為firstChild,由于上面已經(jīng)有一個(gè)docype的子結(jié)點(diǎn)了,所以已經(jīng)有l(wèi)astChild了,因此會(huì)把這個(gè)子元素的previousSibling指向老的lastChild,老的lastChild的nexSibling指向它。***倒數(shù)第二行再把子元素設(shè)置為當(dāng)前ContainerNode(即document)的lastChild。這樣就建立起了html結(jié)點(diǎn)的父子兄弟關(guān)系。

可以看到,借助上一次的m_lastChild建立起了兄弟關(guān)系。

這個(gè)時(shí)候你可能會(huì)有一個(gè)問題,為什么要用一個(gè)task隊(duì)列存放將要插入的結(jié)點(diǎn)呢,而不是直接插入呢?一個(gè)原因是放到task里面方便統(tǒng)一處理,并且有些task可能不能立即執(zhí)行,要先存起來。不過在我們這個(gè)案例里面都是存完后下一步就執(zhí)行了。

當(dāng)遇到head標(biāo)簽的token時(shí),也是先創(chuàng)建一個(gè)head結(jié)點(diǎn),然后再創(chuàng)建一個(gè)task,插到隊(duì)列里面:

void HTMLConstructionSite::insertHTMLHeadElement(AtomicHTMLToken* token) {
  m_head = HTMLStackItem::create(createHTMLElement(token), token);
  attachLater(currentNode(), m_head->element());
  m_openElements.pushHTMLHeadElement(m_head);
}

attachLater傳參的***個(gè)參數(shù)為父結(jié)點(diǎn),這個(gè)currentNode為開標(biāo)簽棧里面的最頂?shù)脑兀?/p>

ContainerNode* currentNode() const { 
    return m_openElements.topNode(); 
}

我們剛剛把html元素壓了進(jìn)去,則棧頂元素為html元素,所以head的父結(jié)點(diǎn)就為html。所以每當(dāng)遇到一個(gè)開標(biāo)簽時(shí),就把它壓起來,下一次再遇到一個(gè)開標(biāo)簽時(shí),它的父元素就是上一個(gè)開標(biāo)簽。

所以,初步可以看到,借助一個(gè)棧建立起了父子關(guān)系。

而當(dāng)遇到一個(gè)閉標(biāo)簽?zāi)兀?/p>

(5)處理閉標(biāo)簽

當(dāng)遇到一個(gè)閉標(biāo)簽時(shí),會(huì)把棧里面的元素一直pop出來,直到pop到***個(gè)和它標(biāo)簽名字一樣的:

m_tree.openElements()->popUntilPopped(token->name());

我們***個(gè)遇到的是閉標(biāo)簽是head標(biāo)簽,它會(huì)把開的head標(biāo)簽pop出來,棧里面就剩下html元素了,所以當(dāng)再遇到body時(shí),html元素就是body的父元素了。

這個(gè)是棧的一個(gè)典型應(yīng)用。

以下面的html為例來研究壓棧和出棧的過程:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"></meta>
</head>
<body>
    <div>
        <p><b>hello</b></p>
        <p>demo</p>
    </div>
</body>
</html>

把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: "

這個(gè)過程確實(shí)和上面的描述一致,遇到一個(gè)閉標(biāo)簽就把一次的開標(biāo)簽pop出來。

并且可以發(fā)現(xiàn)遇到body閉標(biāo)簽后,并不會(huì)把body給pop出來,因?yàn)槿绻鸼ody閉標(biāo)簽后面又再寫了標(biāo)簽的話,就會(huì)自動(dòng)當(dāng)成body的子元素。

假設(shè)上面的b標(biāo)簽的閉標(biāo)簽忘記寫了,又會(huì)發(fā)生什么:

<p><b>hello</p>

打印出來的結(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)簽時(shí),會(huì)把所有的開標(biāo)簽pop出來,直到遇到P標(biāo)簽。不同的是后續(xù)的過程中會(huì)不斷地插入b標(biāo)簽,***渲染的頁面結(jié)構(gòu):

因?yàn)閎等帶有格式化的標(biāo)簽會(huì)特殊處理,遇到一個(gè)開標(biāo)簽時(shí)會(huì)它們放到一個(gè)列表里面:

 // a, b, big, code, em, font, i, nobr, s, small, strike, strong, tt, and u.
  m_activeFormattingElements.append(currentElementRecord()->stackItem());

遇到一個(gè)閉標(biāo)簽時(shí),又會(huì)從這個(gè)列表里面刪掉。每處理一個(gè)新標(biāo)簽時(shí)就會(huì)進(jìn)行檢查和這個(gè)列表和棧里的開標(biāo)簽是否對(duì)應(yīng),如果不對(duì)應(yīng)則會(huì)reconstruct:重新插入一個(gè)開標(biāo)簽。因此b就不斷地被重新插入,直到遇到下一個(gè)b的閉標(biāo)簽為止。

如果上面少寫的是一個(gè)span,那么渲染之后的結(jié)果是正常的:

而對(duì)于文本節(jié)點(diǎn)是實(shí)例化了Text的對(duì)象,這里不再展開討論。

(6)自定義標(biāo)簽的處理

在瀏覽器里面可以看到,自定義標(biāo)簽?zāi)J(rèn)不會(huì)有任何的樣式,并且它默認(rèn)是一個(gè)行內(nèi)元素:

初步觀察它和span標(biāo)簽的表現(xiàn)是一樣的:

在blink的源碼里面,不認(rèn)識(shí)的標(biāo)簽?zāi)J(rèn)會(huì)被實(shí)例化成一個(gè)HTMLUnknownElement,這個(gè)類對(duì)外提供了一個(gè)create函數(shù),這和HTMLSpanElement是一樣的,只有一個(gè)create函數(shù),并且大家都是繼承于HTMLElement。并且創(chuàng)建span標(biāo)簽的時(shí)候和unknown一樣,并沒有做特殊處理,直接調(diào)的create。所以從本質(zhì)上來說,可以把自定義的標(biāo)簽當(dāng)作一個(gè)span看待。然后你可以再設(shè)置display: block改成塊級(jí)元素之類的。

但是你可以用js定義一個(gè)自定義標(biāo)簽,定義它的屬性等,Webkit會(huì)去讀它的定義:

// "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)建一個(gè)原生屬性:

<high-school country="China">NO. 2 high school</high-school>

上面定義了一個(gè)country,為了可以直接獲取這個(gè)屬性:

console.log(document.getElementsByTagName("high-school")[0].country);

注冊(cè)一個(gè)自定義標(biāo)簽:

window.customElements.define("high-school", HighSchoolElement);

這個(gè)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這個(gè)屬性,而不用通過getAttribute的函數(shù),并且可以在屬性發(fā)生變化時(shí)更新元素的渲染,改變color等。詳見Custom Elements – W3C.

通過這種方式創(chuàng)建的,它就不是一個(gè)HTMLUnknownElement了。blink通過V8引擎把js的構(gòu)造函數(shù)轉(zhuǎn)化成C++的函數(shù),實(shí)例化一個(gè)HTMLElement的對(duì)象。

***再來看查DOM的過程

4. 查DOM過程

(1)按ID查找

在頁面添加一個(gè)script:

<script>document.getElementById("text")</script>

Chrome的V8引擎把js代碼層層轉(zhuǎn)化,***會(huì)調(diào):

DocumentV8Internal::getElementByIdMethodForMainWorld(info);

而這個(gè)函數(shù)又會(huì)調(diào)TreeScope的getElementById的函數(shù),TreeScope存儲(chǔ)了一個(gè)m_map的哈希map,這個(gè)map以標(biāo)簽id字符串作為key值,Element為value值,我們可以把這個(gè)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)是這樣的:

<div class="user" id="id-yin">
    <p id="id-name" class="important">yin</p>
    <p id="id-age">20</p>
    <p id="id-sex">mail</p>
</div>

打印出來的結(jié)果為:

"id-age" "P"
"id-sex" "P"
"id-name" "P"
"id-yin" "DIV"

可以看到, 這個(gè)m_map把頁面所有有id的標(biāo)簽都存了進(jìn)來。由于map的查找時(shí)間復(fù)雜度為O(1),所以使用ID選擇器可以說是最快的。

再來看一下類選擇器:

(2)類選擇器

js如下:

var users = document.getElementsByClassName("user"); 
users.length;

在執(zhí)行***行的時(shí)候,Webkit返回了一個(gè)ClassCollection的列表:

return new ClassCollection(rootNode, classNames);

而這個(gè)列表并不是去查DOM獲取的,它只是記錄了className作為標(biāo)志。這與我們的認(rèn)知是一致的,這種HTMLCollection的數(shù)據(jù)結(jié)構(gòu)都是在使用的時(shí)候才去查DOM,所以在上面第二行去獲取它的length,就會(huì)觸發(fā)它的查DOM,在nodeCount這個(gè)函數(shù)里面執(zhí)行:

  NodeType* currentNode = collection.traverseToFirst();
  unsigned currentIndex = 0;
  while (currentNode) {
    m_cachedList.push_back(currentNode);
    currentNode = collection.traverseForwardToOffset(
        currentIndex + 1, *currentNode, currentIndex);
  }

***行先獲取符合collection條件的***個(gè)結(jié)點(diǎn),然后不斷獲取下一個(gè)符合條件的結(jié)點(diǎn),直到null,并把它存到一個(gè)cachedList里面,下次再獲取這個(gè)collection的東西時(shí)便不用再重復(fù)查DOM,只要cached仍然是有效的:

  if (this->isCachedNodeCountValid())
    return this->cachedNodeCount();

怎么樣找到有效的節(jié)點(diǎn)呢:

  ElementType* element = Traversal<ElementType>::firstWithin(current);
  while (element && !isMatch(*element))
    element = Traversal<ElementType>::next(*element, &current, isMatch);
  return element;

***行先獲取***個(gè)節(jié)點(diǎn),如果它沒有match,則繼續(xù)next,直到找到符合條件或者空為止。我們的重點(diǎn)在于,它是怎么遍歷的,如何next獲取下一個(gè)節(jié)點(diǎn),核心代碼:

  if (current.hasChildren())
    return current.firstChild();
  if (current == stayWithin)
    return 0;
  if (current.nextSibling())
    return current.nextSibling();
  return nextAncestorSibling(current, stayWithin);

***行先判斷當(dāng)前節(jié)點(diǎn)有沒有子元素,如果有的話返回它的***個(gè)子元素,如果當(dāng)前節(jié)點(diǎn)沒有子元素,并且這個(gè)節(jié)點(diǎn)就是開始找的根元素(用document.getElement*,則為document),則說明沒有下一個(gè)元素了,直接返回0/null。如果這個(gè)節(jié)點(diǎn)不是根元素了(例如已經(jīng)到了子元素這一層),那么看它有沒有相鄰元素,如果有則返回下一個(gè)相鄰元素,如果相鄰無素也沒有了,由于它是一個(gè)葉子結(jié)點(diǎn)(沒有子元素),說明它已經(jīng)到了最深的一層,并且是當(dāng)前層的***一個(gè)葉子結(jié)點(diǎn),那么就返回它的父元素的下一個(gè)相鄰節(jié)點(diǎn),如果這個(gè)也沒有了,則返回null,查找結(jié)束??梢钥闯鲞@是一個(gè)深度優(yōu)先的查找。

(3)querySelector

a)先來看下selector為一個(gè)id時(shí)發(fā)生了什么:

document.querySelector("#id-name");

它會(huì)調(diào)ContainerNode的querySelecotr函數(shù):

SelectorQuery* selectorQuery = document().selectorQueryCache().add(
      selectors, document(), exceptionState);

return selectorQuery->queryFirst(*this);

先把輸入的selector字符串序列化成一個(gè)selectorQuery,然后再queryFirst,通過打斷點(diǎn)可以發(fā)現(xiàn),它***會(huì)調(diào)的TreeScope的getElementById:

rootNode.treeScope().getElementById(idToMatch);

b)如果selector為一個(gè)class:

document.querySelector(".user");

它會(huì)從document開始遍歷:

  for (Element& element : ElementTraversal::descendantsOf(rootNode)) {
    if (element.hasClass() && element.classNames().contains(className)) {
      SelectorQueryTrait::appendElement(output, element);
      if (SelectorQueryTrait::shouldOnlyMatchFirstElement)
        return;
    }
  }

我們重點(diǎn)查看它是怎么遍歷,即***行的for循環(huán)。表面上看它好像把所有的元素取出來然后做個(gè)循環(huán),其實(shí)不然,它是重載++操作符:

void operator++() { m_current = TraversalNext::next(*m_current, m_root); }

只要我們看下next是怎么操作的就可以得知它是怎么遍歷,而這個(gè)next跟上面的講解class時(shí)是調(diào)的同一個(gè)next。不一樣的是match條件判斷是:有className,并且className列表里面包含這個(gè)class,如上面代碼第二行。

c)復(fù)雜選擇器

例如寫兩個(gè)class:

document.querySelector(".user .important");

最終也會(huì)轉(zhuǎn)成一個(gè)遍歷,只是判斷是否match的條件不一樣:

  for (Element& element : ElementTraversal::descendantsOf(*traverseRoot)) {
    if (selectorMatches(selector, element, rootNode)) {
      SelectorQueryTrait::appendElement(output, element);
      if (SelectorQueryTrait::shouldOnlyMatchFirstElement)
        return;
    }
  }

怎么判斷是否match比較復(fù)雜,這里不再展開討論。

同時(shí)在源碼可以看到,如果是怪異模式,會(huì)調(diào)一個(gè)executeSlow的查詢,并且判斷match條件也不一樣。不過遍歷是一樣的。

查看源碼確實(shí)是一件很費(fèi)時(shí)費(fèi)力的工作,但是通過一番探索,能夠了解瀏覽器的一些內(nèi)在機(jī)制,至少已經(jīng)可以回答上面提出來的幾個(gè)問題。同時(shí)知道了Webkit/Blink借助一個(gè)棧,結(jié)合開閉標(biāo)簽,一步步構(gòu)建DOM樹,并對(duì)DOCType的標(biāo)簽、自定義標(biāo)簽的處理有了一定的了解。***又討論了查DOM的幾種情況,明白了查找的過程。

通過上面的分析,對(duì)頁面渲染的***步構(gòu)建DOM應(yīng)該會(huì)有一個(gè)基礎(chǔ)的了解。

責(zé)任編輯:張燕妮 來源: 會(huì)編程的銀豬
相關(guān)推薦

2017-02-28 10:05:56

Chrome源碼

2017-11-21 14:56:59

2017-02-09 15:15:54

Chrome瀏覽器

2011-06-21 16:52:48

2012-07-04 17:00:06

獵豹瀏覽瀏覽器

2009-11-26 10:55:41

2010-01-28 10:13:43

2015-01-21 15:45:50

斯巴達(dá)瀏覽器

2018-02-02 15:48:47

ChromeDNS解析

2009-12-03 10:56:34

谷歌Chrome瀏覽器

2009-12-06 09:38:02

Chrome瀏覽器Avast

2009-03-07 09:57:41

Realplayer捆綁Chrome

2019-02-15 15:15:59

ChromeJavascriptHtml

2010-01-10 17:50:17

2009-07-17 09:16:20

Google Chro瀏覽器操作系統(tǒng)

2009-09-22 09:17:46

谷歌Chrome瀏覽器

2012-08-08 09:18:47

Chrome瀏覽器

2013-11-13 15:54:20

Chrome 31瀏覽器

2020-11-25 09:47:11

FedoraGoogle Chro瀏覽器

2022-02-07 21:49:06

瀏覽器渲染chromium
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)