一個老生常談的問題,從輸入url到頁面渲染完成之間發(fā)生了什么?
在這個過程中包括以下2大部分:
- 1.http請求響應(yīng)
- 2.渲染
先來提三個問題:
1.當(dāng)輸入url后,瀏覽器如何包裝發(fā)起請求?
2.在發(fā)出請求--接到響應(yīng)之間發(fā)生了什么?
3.當(dāng)返回請求結(jié)果后,瀏覽器如何解析結(jié)果?
1.為了知道瀏覽器是如何包裝http請求的,使用nodejs搭建服務(wù)器
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.服務(wù)器搭建好了,需要知道瀏覽器到底包裝了什么信息,直接看控制臺:
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請求包裝
這些是瀏覽器自動包裝過后的請求,包括請求行,請求頭和請求主體,瀏覽器默認(rèn)發(fā)送的是GET請求,如果需要指定POST請求,可以寫個表單來驗證一下,大概意思是瀏覽器發(fā)起post請求,服務(wù)端接收到后返回success,瀏覽器端顯示返回的內(nèi)容
//index.html
這樣寫的時候,由于html文件的協(xié)議是file,所以為了解決跨域問題,需要服務(wù)端進(jìn)行設(shè)置
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請求就成功了,來看看瀏覽器默認(rèn)包裝了什么信息
Request URL: http://localhost:8005/ Request Method: POST Status Code: 200 OK Remote Address: [::1]:8005 //自動使用https協(xié)議 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 整體流程前面已經(jīng)知道了瀏覽器在發(fā)起GET或者POST請求的時候會自動的添加的字段,那瀏覽器在發(fā)送請求后到接收到服務(wù)端傳來的數(shù)據(jù)前這段時間發(fā)生了什么?
網(wǎng)上看到大家的回答大部分都是:
1.接收 URL,并拆分成協(xié)議,網(wǎng)絡(luò)地址,資源路徑
2.與緩存進(jìn)行比對,如果請求的對象在緩存中,則直接進(jìn)行第9步
3.檢查域名是否在本地的 host 的文件中,在則直接返回 IP 地址,不在則向 DNS 服務(wù)器請求,直到查詢到 IP 地址
4.瀏覽器向服務(wù)器發(fā)起一個 TCP 連接
5.瀏覽器通過 TCP 連接向服務(wù)器發(fā)起 HTTP 請求,HTTP 三次握手,HTTPS 握手過程則復(fù)雜得多
6.瀏覽器接受 HTTP 響應(yīng),這時候它能關(guān)閉 TCP 連接也能為另一個連接保留。
7.檢查 HTTP header 里的狀態(tài)碼,并做出不同的處理方式。比如:錯誤(4XX、5XX),重定向(3XX),授權(quán)請求(2XX)
8.如果是可以緩存的,這個響應(yīng)則會被存儲起來
9.瀏覽器進(jìn)行解碼響應(yīng),并決定如何處理該響應(yīng)(比如HTML頁面,圖像,聲音等等)
10.瀏覽器渲染響應(yīng),或者為不能識別的類型提供下載的提示框
1.2.2 域名解析流程這樣的回答確實把相關(guān)的流程說了一遍,但是DNS是如何把域名解析成IP的?這個過程可以被觀察到么?三次握手又是什么意思?
為了看到域名解析的過程,我們可以使用Nslookup,它是由微軟發(fā)布用于對DNS服務(wù)器進(jìn)行檢測和排錯的命令行工具
比如可以看一下,https://www.baidu.com它的IP是什么,nslookup https://www.baidu.com
我在查看的時候一直報延時錯誤,只好從網(wǎng)上引用一張圖來說明一下了
其中server代表本地地址ip,下面那個address是百度的ip
通過這樣的方式就能看到具體域名解析的過程
接下來是三次握手,當(dāng)域名轉(zhuǎn)化成IP后,瀏覽器沿著ip找到服務(wù)器,進(jìn)行三次握手:
第一次握手:客戶端的應(yīng)用進(jìn)程主動打開,并向客戶端發(fā)出請求報文段。其首部中:SYN=1,seq=x。
第二次握手:服務(wù)器應(yīng)用進(jìn)程被動打開。若同意客戶端的請求,則發(fā)回確認(rèn)報文,其首部中:SYN=1,ACK=1,ack=x+1,seq=y
第三次握手:客戶端收到確認(rèn)報文之后,通知上層應(yīng)用進(jìn)程連接已建立,并向服務(wù)器發(fā)出確認(rèn)報文,其首部:ACK=1,ack=y+1。當(dāng)服務(wù)器收到客戶端的確認(rèn)報文之后,也通知其上層應(yīng)用進(jìn)程連接已建立
看到這里,有個問題,前兩次握手已經(jīng)把客戶端和服務(wù)端聯(lián)系在一起了,那為什么還要第三次握手?
如果是兩次握手,當(dāng)A想要建立連接時發(fā)送一個SYN,然后等待ACK,結(jié)果這個SYN因為網(wǎng)絡(luò)問題沒有及時到達(dá)B,所以A在一段時間內(nèi)沒收到ACK后,在發(fā)送一個SYN,B也成功收到,然后A也收到ACK,這時A發(fā)送的第一個SYN終于到了B,對于B來說這是一個新連接請求,然后B又為這個連接申請資源,返回ACK,然而這個SYN是個無效的請求,A收到這個SYN的ACK后也并不會理會它,而B卻不知道,B會一直為這個連接維持著資源,造成資源的浪費,但如果是三次握手,如果第三次握手遲遲不來,服務(wù)器便會認(rèn)為這個SYN是無效的,釋放相關(guān)資源1.3 響應(yīng)
成功發(fā)起請求并完整走完了上述流程,瀏覽器能獲得服務(wù)器發(fā)來的數(shù)據(jù),那這些數(shù)據(jù)被放在哪里,它是如何被瀏覽器處理的?
其實這個問題很簡單,在前面成功發(fā)起http請求后,服務(wù)端會有一個響應(yīng),這里面規(guī)定了各種文件格式
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 整體流程
數(shù)據(jù)請求回來以后,瀏覽器是如何把數(shù)據(jù)轉(zhuǎn)化成頁面的呢?這個過程就涉及到了DOM樹,CSSOM樹,render樹的生成和頁面的繪制,先來貼圖看看整體流程:
在構(gòu)建DOM樹的時候,遇到 js 和 CSS元素,HTML解析器就換將控制權(quán)轉(zhuǎn)讓給JS解析器或者是CSS解析器。開始構(gòu)建CSSOM,在構(gòu)建CSSOM樹的時候,解析是從右向左進(jìn)行的,DOM樹構(gòu)建完之后和CSSOM合成一棵render tree
有了Render Tree,瀏覽器已經(jīng)能知道網(wǎng)頁中有哪些節(jié)點、各個節(jié)點的CSS定義以及他們的從屬關(guān)系。下一步操作稱之為Layout,顧名思義就是計算出每個節(jié)點在屏幕中的位置
Layout后,瀏覽器已經(jīng)知道了哪些節(jié)點要顯示(which nodes are visible)、每個節(jié)點的CSS屬性是什么(their computed styles)、每個節(jié)點在屏幕中的位置是哪里(geometry)。就進(jìn)入了最后一步:Painting,按照算出來的規(guī)則,通過顯卡,把內(nèi)容畫到屏幕上,HTML默認(rèn)是流式布局的,CSS和js會打破這種布局,改變DOM的外觀樣式以及大小和位置,當(dāng)尺寸改變時會reflow,也就是重新繪制,比如table布局整體尺寸改變,頁面就需要重繪,但當(dāng)非尺寸改變時,會進(jìn)行replaint
通過這個分析知道了DOM樹的生成過程中可能會被CSS和JS的加載執(zhí)行阻塞,所以平時寫CSS時,盡量用id和class,千萬不要過渡層疊,盡量減少會造成reflow的操作,把JS代碼放到頁面底部,且JavaScript 應(yīng)盡量少影響 DOM 的構(gòu)建2.2 底層源碼
這樣說一遍,還是在很表面的層次在說渲染這件事,那有沒有更深層次的理解呢?可以通過看瀏覽器源碼來進(jìn)行分析:
大致分為三個步驟:
1.HTMLDocumentParser負(fù)責(zé)解析html文本為tokens
2.HTMLTreeBuilder對這些tokens分類處理
3.HTMLConstructionSite調(diào)用不同的函數(shù)構(gòu)建DOM樹
接下來使用這個html文檔來說明DOM樹的構(gòu)建過程:
2.2.1生成tokensdemo
首先是>>>HTMLDocumentParser負(fù)責(zé)解析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);//內(nèi)部調(diào)用HTMLDocumentParser }
構(gòu)建出來的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 構(gòu)建DOM樹
最后,最關(guān)鍵的就是HTMLConstructionSite調(diào)用不同的函數(shù)構(gòu)建DOM樹,它根據(jù)不同的節(jié)點類型進(jìn)行不同的處理
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的寫法,標(biāo)準(zhǔn)模式 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解析會比較寬松,例如數(shù)字單位可以省略, //有限怪異模式和標(biāo)準(zhǔn)模式的唯一區(qū)別在于在于對inline元素的行高處理不一樣 //標(biāo)準(zhǔn)模式將會讓頁面遵守文檔規(guī)定2.開標(biāo)簽的處理
首先是標(biāo)簽,處理這個標(biāo)簽的任務(wù)應(yīng)該是實例化一個HTMLHtmlElement元素,然后把它的父元素指向document
HTMLConstructionSite::HTMLConstructionSite( Document& document) : m_document(&document), m_attachmentRoot(document)) { }
void HTMLConstructionSite::insertHTMLHtmlStartTagBeforeHTML(AtomicHTMLToken* token) { HTMLHtmlElement* element = HTMLHtmlElement::create(*m_document);//創(chuàng)建一個html結(jié)點 attachLater(m_attachmentRoot, element);//加到一個任務(wù)隊列里面 m_openElements.pushHTMLHtmlElement(HTMLStackItem::create(element, token));//壓到一個棧里面,這個棧存放了未遇到閉標(biāo)簽的所有開標(biāo)簽 executeQueuedTasks();//執(zhí)行隊列里面的任務(wù) }
//建立一個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根據(jù)task的類型執(zhí)行不同的操作 void ContainerNode::parserAppendChild(Node* newChild) { if (!checkParserAcceptChild(*newChild)) return; AdoptAndAppendChild()(*this, *newChild, nullptr); } notifyNodeInserted(*newChild, ChildrenChangeSourceParser); }
//建立起html結(jié)點的父子兄弟關(guān)系 void ContainerNode::appendChildCommon(Node& child) { child.setParentOrShadowHostNode(this);//設(shè)置子元素的父結(jié)點,也就是會把html結(jié)點的父結(jié)點指向document if (m_lastChild) { //子元素的previousSibling指向老的lastChild,老的lastChild的nexSibling指向它 child.setPreviousSibling(m_lastChild); m_lastChild->setNextSibling(&child); } else { //如果沒有l(wèi)astChild,會將這個子元素作為firstChild setFirstChild(&child); } //子元素設(shè)置為當(dāng)前ContainerNode(即document)的lastChild setLastChild(&child); }
每當(dāng)遇到一個開標(biāo)簽時,就把它壓起來,下一次再遇到一個開標(biāo)簽時,它的父元素就是上一個開標(biāo)簽,借助一個棧建立起了父子關(guān)系
3.閉標(biāo)簽的處理第一個閉標(biāo)簽是head標(biāo)簽,它會把開的head標(biāo)簽pop出來,棧里面就剩下html元素了,所以當(dāng)再遇到body時,html元素就是body的父元素了
m_tree.openElements()->popUntilPopped(token->name());
至此,一個url到頁面的過程差不多就完成了,寫這篇參考了很多文章,鏈接貼在下面,大家可以去看看:
1.簡述TCP連接的建立與釋放(三次握手、四次揮手):https://www.cnblogs.com/zhuwq...
2.從輸入 URL 到頁面加載完成發(fā)生了什么事:https://segmentfault.com/a/11...
3.十分鐘讀懂瀏覽器渲染流程:https://segmentfault.com/a/11...
4.從Chrome源碼看瀏覽器如何構(gòu)建DOM樹 :https://zhuanlan.zhihu.com/p/...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/54995.html
摘要:大多數(shù)情況,為了安全考慮,瀏覽器會強(qiáng)制使用同源策略,意味著一個源無法訪問另一個源的數(shù)據(jù)。如果想要從加載一個文件,它就需要在實行同源策略的瀏覽中發(fā)起一個跨域資源請求。 原文:https://alistapart.com/articl... 最近發(fā)現(xiàn)國外有一個系列,專門探究從輸入URL到頁面可交互的詳細(xì)過程,是一份干貨十足的好資料。筆者決定分為四篇文章對其進(jìn)行有刪減地翻譯,只希望能對大家...
摘要:通過前端路由可以實現(xiàn)單頁應(yīng)用本文首先從前端路由的原理出發(fā),詳細(xì)介紹了前端路由原理的變遷。接著從的源碼出發(fā),深入理解是如何實現(xiàn)前端路由的。執(zhí)行上述的賦值后,頁面的發(fā)生改變。 ??react-router等前端路由的原理大致相同,可以實現(xiàn)無刷新的條件下切換顯示不同的頁面。路由的本質(zhì)就是頁面的URL發(fā)生改變時,頁面的顯示結(jié)果可以根據(jù)URL的變化而變化,但是頁面不會刷新。通過前端路由可以實現(xiàn)...
摘要:所以再做頁面跳轉(zhuǎn)的時候如果不想留下記錄,還是用比較保險,如果想留下記錄,應(yīng)該幾百毫秒再跳轉(zhuǎn)。解決辦法先用給瀏覽器添加一條記錄,然后用的方法替換掉添加的記錄,這樣記錄里存的就是和解決方案 空 location.href = url location.reload() location.replace(url) url完全不變的情況下 刷新Docment,不會產(chǎn)生記錄 刷新Doc...
摘要:本地服務(wù)器收到信息后,再去聯(lián)系頂級域名服務(wù)器。頂級域名服務(wù)器收到請求后,如果自己無法解析,再返回下一級域名服務(wù)器的,進(jìn)行這樣一個迭代查詢之后,一直到子域名服務(wù)器。布局完成后,將渲染樹轉(zhuǎn)換成屏幕上的像素,顯示頁面。 當(dāng)我們輸入 URL 并按回車后,瀏覽器會對 URL 進(jìn)行檢查,首先判斷URL格式,比如是ftp http ed2k等等,我們這里假設(shè)這個URL是http://hellocas...
摘要:本地服務(wù)器收到信息后,再去聯(lián)系頂級域名服務(wù)器。頂級域名服務(wù)器收到請求后,如果自己無法解析,再返回下一級域名服務(wù)器的,進(jìn)行這樣一個迭代查詢之后,一直到子域名服務(wù)器。布局完成后,將渲染樹轉(zhuǎn)換成屏幕上的像素,顯示頁面。 當(dāng)我們輸入 URL 并按回車后,瀏覽器會對 URL 進(jìn)行檢查,首先判斷URL格式,比如是ftp http ed2k等等,我們這里假設(shè)這個URL是http://hellocas...
閱讀 2323·2021-10-08 10:04
閱讀 1097·2021-09-03 10:40
閱讀 1150·2019-08-30 15:53
閱讀 3309·2019-08-30 13:13
閱讀 2925·2019-08-30 12:55
閱讀 2278·2019-08-29 13:21
閱讀 1330·2019-08-26 12:12
閱讀 2755·2019-08-26 10:37