日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
從Chrome源碼看瀏覽器如何構(gòu)建DOM樹

這幾天下了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證書合作)期待與您的合作!

  1. IE用的是Trident內(nèi)核,Safari用的是Webkit,Chrome用的是Blink,到底什么是內(nèi)核,它們的區(qū)別是什么?
  2. 如果沒有聲明會造成什么影響?
  3. 瀏覽器如何處理自定義的標(biāo)簽,如寫一個
  4. 查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

mail

打印出來的結(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