從URL到頁面

一個老生常談的問題,從輸入url到頁面渲染完成之間發生了什麼?

在這個過程當中包括如下2大部分:html

- 1.http請求響應
- 2.渲染node

1.http請求響應

先來提三個問題:
1.當輸入url後,瀏覽器如何包裝發起請求?
2.在發出請求--接到響應之間發生了什麼?
3.當返回請求結果後,瀏覽器如何解析結果?web

1.1 請求

1.1.1 GET請求包裝

1.爲了知道瀏覽器是如何包裝http請求的,使用nodejs搭建服務器segmentfault

const http = require('http');

const server = http.createServer((req,res) => {
    if(req.url === '/'){
        res.end('hello')
    }
});

server.listen(8005,() => {
    console.log('server listen on http://localhost:8005')
});

2.服務器搭建好了,須要知道瀏覽器到底包裝了什麼信息,直接看控制檯:後端

Request URL: http://localhost:8005/
Request Method: GET
Status Code: 200 OK
Remote Address: [::1]:8005
Referrer Policy: no-referrer-when-downgrade
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: max-age=0
Connection: keep-alive
Host: localhost:8005
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36

1.1.2 POST請求包裝

這些是瀏覽器自動包裝事後的請求,包括請求行,請求頭和請求主體,瀏覽器默認發送的是GET請求,若是須要指定POST請求,能夠寫個表單來驗證一下,大概意思是瀏覽器發起post請求,服務端接收到後返回success,瀏覽器端顯示返回的內容跨域

//index.html
<!DOCTYPE HTML>
<html>
<body>

<form>
  <input type="text" id="val"/>
</form>
<button id="button">submit</button>
<div id="item"></div>

<script>
    var val = document.getElementById('val');
    var button = document.getElementById('button');
    var item = document.getElementById('item');

    button.addEventListener('click',function(){
    
        var oAjax = new XMLHttpRequest();
        oAjax.open('POST', 'http://localhost:8005', false);
        oAjax.setRequestHeader("Content-type", "application/*");
        var data = {
            value:val.value
        };
        oAjax.onreadystatechange = function() {
                    if (oAjax.readyState == 4 && oAjax.status == 200) {
                        item.innerHTML = oAjax.responseText;
                    } else {
                        console.log(oAjax);
                    }
                };
        oAjax.send(JSON.stringify(data));


    })

</script>

</body>
</html>

這樣寫的時候,因爲html文件的協議是file,因此爲了解決跨域問題,須要服務端進行設置瀏覽器

const http = require('http');

const server = http.createServer((req,res) => {
    if(req.url === '/'){
        res.setHeader("Access-Control-Allow-Origin", "*")
        res.setHeader("Access-Control-Allow-methods", "GET, POST, OPTIONS, PUT, DELETE")
        res.setHeader("Access-Control-Allow-Headers","*")
        res.setHeader("Content-type","application/plain")
        res.end('success!!!')
    }
});

server.listen(8005,() => {
    console.log('server listen on http://localhost:8005')
});

http
這樣一次post請求就成功了,來看看瀏覽器默認包裝了什麼信息緩存

Request URL: http://localhost:8005/
Request Method: POST
Status Code: 200 OK
Remote Address: [::1]:8005
//自動使用https協議
Referrer Policy: no-referrer-when-downgrade
Content-type: application/*
Origin: null
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36

這些信息有的是咱們本身在後端寫的,有的是瀏覽器自動添加的服務器

1.2 過程

1.2.1 總體流程

前面已經知道了瀏覽器在發起GET或者POST請求的時候會自動的添加的字段,那瀏覽器在發送請求後到接收到服務端傳來的數據前這段時間發生了什麼?
網上看到你們的回答大部分都是:網絡

  • 1.接收 URL,並拆分紅協議,網絡地址,資源路徑
  • 2.與緩存進行比對,若是請求的對象在緩存中,則直接進行第9步
  • 3.檢查域名是否在本地的 host 的文件中,在則直接返回 IP 地址,不在則向 DNS 服務器請求,直到查詢到 IP 地址
  • 4.瀏覽器向服務器發起一個 TCP 鏈接
  • 5.瀏覽器經過 TCP 鏈接向服務器發起 HTTP 請求,HTTP 三次握手,HTTPS 握手過程則複雜得多
  • 6.瀏覽器接受 HTTP 響應,這時候它能關閉 TCP 鏈接也能爲另外一個鏈接保留。
  • 7.檢查 HTTP header 裏的狀態碼,並作出不一樣的處理方式。好比:錯誤(4XX、5XX),重定向(3XX),受權請求(2XX)
  • 8.若是是能夠緩存的,這個響應則會被存儲起來
  • 9.瀏覽器進行解碼響應,並決定如何處理該響應(好比HTML頁面,圖像,聲音等等)
  • 10.瀏覽器渲染響應,或者爲不能識別的類型提供下載的提示框

1.2.2 域名解析流程

這樣的回答確實把相關的流程說了一遍,可是DNS是如何把域名解析成IP的?這個過程能夠被觀察到麼?三次握手又是什麼意思?
爲了看到域名解析的過程,咱們可使用Nslookup,它是由微軟發佈用於對DNS服務器進行檢測和排錯的命令行工具
好比能夠看一下,https://www.baidu.com它的IP是什麼,nslookup https://www.baidu.com
我在查看的時候一直報延時錯誤,只好從網上引用一張圖來講明一下了
nslookup
其中server表明本地地址ip,下面那個address是百度的ip
經過這樣的方式就能看到具體域名解析的過程

1.2.3 三次握手流程

接下來是三次握手,當域名轉化成IP後,瀏覽器沿着ip找到服務器,進行三次握手:

  • 第一次握手:客戶端的應用進程主動打開,並向客戶端發出請求報文段。其首部中:SYN=1,seq=x。
  • 第二次握手:服務器應用進程被動打開。若贊成客戶端的請求,則發回確認報文,其首部中:SYN=1,ACK=1,ack=x+1,seq=y
  • 第三次握手:客戶端收到確認報文以後,通知上層應用進程鏈接已創建,並向服務器發出確認報文,其首部:ACK=1,ack=y+1。當服務器收到客戶端的確認報文以後,也通知其上層應用進程鏈接已創建

hand
看到這裏,有個問題,前兩次握手已經把客戶端和服務端聯繫在一塊兒了,那爲何還要第三次握手?

若是是兩次握手,當A想要創建鏈接時發送一個SYN,而後等待ACK,結果這個SYN由於網絡問題沒有及時到達B,因此A在一段時間內沒收到ACK後,在發送一個SYN,B也成功收到,而後A也收到ACK,這時A發送的第一個SYN終於到了B,對於B來講這是一個新鏈接請求,而後B又爲這個鏈接申請資源,返回ACK,然而這個SYN是個無效的請求,A收到這個SYN的ACK後也並不會理會它,而B殊不知道,B會一直爲這個鏈接維持着資源,形成資源的浪費,但若是是三次握手,若是第三次握手遲遲不來,服務器便會認爲這個SYN是無效的,釋放相關資源

1.3 響應

成功發起請求並完整走完了上述流程,瀏覽器能得到服務器發來的數據,那這些數據被放在哪裏,它是如何被瀏覽器處理的?
其實這個問題很簡單,在前面成功發起http請求後,服務端會有一個響應,這裏面規定了各類文件格式

Access-Control-Allow-Headers: *
Access-Control-Allow-methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 10
Content-type: application/plain
Date: Wed, 08 May 2019 07:12:14 GMT

2.渲染

2.1 總體流程

數據請求回來之後,瀏覽器是如何把數據轉化成頁面的呢?這個過程就涉及到了DOM樹,CSSOM樹,render樹的生成和頁面的繪製,先來貼圖看看總體流程:
a
在構建DOM樹的時候,遇到 js 和 CSS元素,HTML解析器就換將控制權轉讓給JS解析器或者是CSS解析器。開始構建CSSOM,在構建CSSOM樹的時候,解析是從右向左進行的,DOM樹構建完以後和CSSOM合成一棵render tree
有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係。下一步操做稱之爲Layout,顧名思義就是計算出每一個節點在屏幕中的位置
Layout後,瀏覽器已經知道了哪些節點要顯示(which nodes are visible)、每一個節點的CSS屬性是什麼(their computed styles)、每一個節點在屏幕中的位置是哪裏(geometry)。就進入了最後一步:Painting,按照算出來的規則,經過顯卡,把內容畫到屏幕上,HTML默認是流式佈局的,CSS和js會打破這種佈局,改變DOM的外觀樣式以及大小和位置,當尺寸改變時會reflow,也就是從新繪製,好比table佈局總體尺寸改變,頁面就須要重繪,但當非尺寸改變時,會進行replaint

經過這個分析知道了DOM樹的生成過程當中可能會被CSS和JS的加載執行阻塞,因此平時寫CSS時,儘可能用id和class,千萬不要過渡層疊,儘可能減小會形成reflow的操做,把JS代碼放到頁面底部,且JavaScript 應儘可能少影響 DOM 的構建

2.2 底層源碼

這樣說一遍,仍是在很表面的層次在說渲染這件事,那有沒有更深層次的理解呢?能夠經過看瀏覽器源碼來進行分析:
大體分爲三個步驟:

  • 1.HTMLDocumentParser負責解析html文本爲tokens
  • 2.HTMLTreeBuilder對這些tokens分類處理
  • 3.HTMLConstructionSite調用不一樣的函數構建DOM樹

parser
接下來使用這個html文檔來講明DOM樹的構建過程:

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

2.2.1生成tokens

首先是>>>HTMLDocumentParser負責解析html文本爲tokens

void DocumentLoader::commitData(const char* bytes, size_t length) {
  ensureWriter(m_response.mimeType());
  if (length)
    m_dataReceived = true;
  m_writer->addData(bytes, length);//內部調用HTMLDocumentParser
}

構建出來的token是包含頁面元素的信息表:

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

2.2.2tokens分類

接着是>>>>>HTMLTreeBuilder對這些tokens分類處理

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
  }
}

2.2.3 構建DOM樹

最後,最關鍵的就是HTMLConstructionSite調用不一樣的函數構建DOM樹,它根據不一樣的節點類型進行不一樣的處理

1.DOCTYPE的處理

// tagName不是html,那麼文檔類型將會是怪異模式
  if (name != "html" ) {
    setCompatibilityMode(Document::QuirksMode);
    return;
  }
// html4寫法,文檔類型是有限怪異模式
  if (!systemId.isEmpty() &&
       publicId.startsWith("-//W3C//DTD HTML 4.01 Transitional//",
                           TextCaseASCIIInsensitive))) {
    setCompatibilityMode(Document::LimitedQuirksMode);
    return;
  }
// h5的寫法,標準模式
  setCompatibilityMode(Document::NoQuirksMode);

不一樣的模式會形成什麼影響?

// 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解析會比較寬鬆,例如數字單位能夠省略,
  //有限怪異模式和標準模式的惟一區別在於在於對inline元素的行高處理不同
  //標準模式將會讓頁面遵照文檔規定

2.開標籤的處理

首先是<html>標籤,處理這個標籤的任務應該是實例化一個HTMLHtmlElement元素,而後把它的父元素指向document

HTMLConstructionSite::HTMLConstructionSite(
    Document& document)
    : m_document(&document),
      m_attachmentRoot(document)) {
}
void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomicHTMLToken* token) {
  HTMLHtmlElement* element = HTMLHtmlElement::create(*m_document);//建立一個html結點
  attachLater(m_attachmentRoot, element);//加到一個任務隊列裏面
  m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element, token));//壓到一個棧裏面,這個棧存放了未遇到閉標籤的全部開標籤
  executeQueuedTasks();//執行隊列裏面的任務
}
//創建一個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);
}
//executeQueuedTasks根據task的類型執行不一樣的操做
void ContainerNode::parserAppendChild(Node* newChild) {
  if (!checkParserAcceptChild(*newChild))
    return;
    AdoptAndAppendChild()(*this, *newChild, nullptr);
  }
  notifyNodeInserted(*newChild, ChildrenChangeSourceParser);
}
//創建起html結點的父子兄弟關係
void ContainerNode::appendChildCommon(Node& child) {
  child.setParentOrShadowHostNode(this);//設置子元素的父結點,也就是會把html結點的父結點指向document
  if (m_lastChild) {
  //子元素的previousSibling指向老的lastChild,老的lastChild的nexSibling指向它
    child.setPreviousSibling(m_lastChild);
    m_lastChild->setNextSibling(&child);
  } else {
      //若是沒有lastChild,會將這個子元素做爲firstChild
    setFirstChild(&child);
  }
  //子元素設置爲當前ContainerNode(即document)的lastChild
  setLastChild(&child);
}

每當遇到一個開標籤時,就把它壓起來,下一次再遇到一個開標籤時,它的父元素就是上一個開標籤,藉助一個棧創建起了父子關係

3.閉標籤的處理

第一個閉標籤是head標籤,它會把開的head標籤pop出來,棧裏面就剩下html元素了,因此當再遇到body時,html元素就是body的父元素了

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

至此,一個url到頁面的過程差很少就完成了,寫這篇參考了不少文章,連接貼在下面,你們能夠去看看:
1.簡述TCP鏈接的創建與釋放(三次握手、四次揮手):https://www.cnblogs.com/zhuwq...
2.從輸入 URL 到頁面加載完成發生了什麼事:https://segmentfault.com/a/11...
3.十分鐘讀懂瀏覽器渲染流程:https://segmentfault.com/a/11...
4.從Chrome源碼看瀏覽器如何構建DOM樹 :https://zhuanlan.zhihu.com/p/...

相關文章
相關標籤/搜索