一個老生常談的問題,從輸入url到頁面渲染完成之間發生了什么?
在這個過程中包括以下2大部分:
- 1.http請求響應
- 2.渲染
先來提三個問題:
1.當輸入url后,瀏覽器如何包裝發起請求?
2.在發出請求--接到響應之間發生了什么?
3.當返回請求結果后,瀏覽器如何解析結果?
1.為了知道瀏覽器是如何包裝http請求的,使用nodejs搭建服務器
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.361.1.2 POST請求包裝
這些是瀏覽器自動包裝過后的請求,包括請求行,請求頭和請求主體,瀏覽器默認發送的是GET請求,如果需要指定POST請求,可以寫個表單來驗證一下,大概意思是瀏覽器發起post請求,服務端接收到后返回success,瀏覽器端顯示返回的內容
//index.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") });
這樣一次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
我在查看的時候一直報延時錯誤,只好從網上引用一張圖來說明一下了
其中server代表本地地址ip,下面那個address是百度的ip
通過這樣的方式就能看到具體域名解析的過程
接下來是三次握手,當域名轉化成IP后,瀏覽器沿著ip找到服務器,進行三次握手:
第一次握手:客戶端的應用進程主動打開,并向客戶端發出請求報文段。其首部中:SYN=1,seq=x。
第二次握手:服務器應用進程被動打開。若同意客戶端的請求,則發回確認報文,其首部中:SYN=1,ACK=1,ack=x+1,seq=y
第三次握手:客戶端收到確認報文之后,通知上層應用進程連接已建立,并向服務器發出確認報文,其首部:ACK=1,ack=y+1。當服務器收到客戶端的確認報文之后,也通知其上層應用進程連接已建立
看到這里,有個問題,前兩次握手已經把客戶端和服務端聯系在一起了,那為什么還要第三次握手?
如果是兩次握手,當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 GMT2.渲染 2.1 整體流程
數據請求回來以后,瀏覽器是如何把數據轉化成頁面的呢?這個過程就涉及到了DOM樹,CSSOM樹,render樹的生成和頁面的繪制,先來貼圖看看整體流程:
在構建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樹
接下來使用這個html文檔來說明DOM樹的構建過程:
2.2.1生成tokensdemo
首先是>>>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: " tagName: html |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: head |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: meta |type: startTag |attr:charset=utf-8 |text: " tagName: |type: Character |attr: |text: " tagName: head |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: body |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: div |type: startTag |attr: |text: " tagName: |type: Character |attr: |text: " 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: " tagName: input |type: startTag |attr:value=hello |text: " tagName: |type: Character |attr: |text: " tagName: div |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: body |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: " tagName: html |type: EndTag |attr: |text: " tagName: |type: Character |attr: |text: " 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.開標簽的處理
首先是標簽,處理這個標簽的任務應該是實例化一個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/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/54995.html
摘要:大多數情況,為了安全考慮,瀏覽器會強制使用同源策略,意味著一個源無法訪問另一個源的數據。如果想要從加載一個文件,它就需要在實行同源策略的瀏覽中發起一個跨域資源請求。 原文:https://alistapart.com/articl... 最近發現國外有一個系列,專門探究從輸入URL到頁面可交互的詳細過程,是一份干貨十足的好資料。筆者決定分為四篇文章對其進行有刪減地翻譯,只希望能對大家...
摘要:通過前端路由可以實現單頁應用本文首先從前端路由的原理出發,詳細介紹了前端路由原理的變遷。接著從的源碼出發,深入理解是如何實現前端路由的。執行上述的賦值后,頁面的發生改變。 ??react-router等前端路由的原理大致相同,可以實現無刷新的條件下切換顯示不同的頁面。路由的本質就是頁面的URL發生改變時,頁面的顯示結果可以根據URL的變化而變化,但是頁面不會刷新。通過前端路由可以實現...
摘要:所以再做頁面跳轉的時候如果不想留下記錄,還是用比較保險,如果想留下記錄,應該幾百毫秒再跳轉。解決辦法先用給瀏覽器添加一條記錄,然后用的方法替換掉添加的記錄,這樣記錄里存的就是和解決方案 空 location.href = url location.reload() location.replace(url) url完全不變的情況下 刷新Docment,不會產生記錄 刷新Doc...
摘要:本地服務器收到信息后,再去聯系頂級域名服務器。頂級域名服務器收到請求后,如果自己無法解析,再返回下一級域名服務器的,進行這樣一個迭代查詢之后,一直到子域名服務器。布局完成后,將渲染樹轉換成屏幕上的像素,顯示頁面。 當我們輸入 URL 并按回車后,瀏覽器會對 URL 進行檢查,首先判斷URL格式,比如是ftp http ed2k等等,我們這里假設這個URL是http://hellocas...
摘要:本地服務器收到信息后,再去聯系頂級域名服務器。頂級域名服務器收到請求后,如果自己無法解析,再返回下一級域名服務器的,進行這樣一個迭代查詢之后,一直到子域名服務器。布局完成后,將渲染樹轉換成屏幕上的像素,顯示頁面。 當我們輸入 URL 并按回車后,瀏覽器會對 URL 進行檢查,首先判斷URL格式,比如是ftp http ed2k等等,我們這里假設這個URL是http://hellocas...
閱讀 2332·2021-10-08 10:04
閱讀 1105·2021-09-03 10:40
閱讀 1158·2019-08-30 15:53
閱讀 3315·2019-08-30 13:13
閱讀 2932·2019-08-30 12:55
閱讀 2286·2019-08-29 13:21
閱讀 1355·2019-08-26 12:12
閱讀 2761·2019-08-26 10:37