一个老生常谈的问题,从输入url到页面渲染完成之间发生了什么?
在这个过程当中包括如下2大部分:
html
- 1.http请求响应
- 2.渲染node
先来提三个问题:
1.当输入url后,浏览器如何包装发起请求?
2.在发出请求--接到响应之间发生了什么?
3.当返回请求结果后,浏览器如何解析结果?
web
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
这些是浏览器自动包装事后的请求,包括请求行,请求头和请求主体,浏览器默认发送的是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') });
这样一次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
这些信息有的是咱们本身在后端写的,有的是浏览器自动添加的
服务器
前面已经知道了浏览器在发起GET或者POST请求的时候会自动的添加的字段,那浏览器在发送请求后到接收到服务端传来的数据前这段时间发生了什么?
网上看到你们的回答大部分都是:
网络
这样的回答确实把相关的流程说了一遍,可是DNS是如何把域名解析成IP的?这个过程能够被观察到么?三次握手又是什么意思?
为了看到域名解析的过程,咱们可使用Nslookup,它是由微软发布用于对DNS服务器进行检测和排错的命令行工具
好比能够看一下,https://www.baidu.com它的IP是什么,nslookup https://www.baidu.com
我在查看的时候一直报延时错误,只好从网上引用一张图来讲明一下了
其中server表明本地地址ip,下面那个address是百度的ip
经过这样的方式就能看到具体域名解析的过程
接下来是三次握手,当域名转化成IP后,浏览器沿着ip找到服务器,进行三次握手:
看到这里,有个问题,前两次握手已经把客户端和服务端联系在一块儿了,那为何还要第三次握手?
若是是两次握手,当A想要创建链接时发送一个SYN,而后等待ACK,结果这个SYN由于网络问题没有及时到达B,因此A在一段时间内没收到ACK后,在发送一个SYN,B也成功收到,而后A也收到ACK,这时A发送的第一个SYN终于到了B,对于B来讲这是一个新链接请求,而后B又为这个链接申请资源,返回ACK,然而这个SYN是个无效的请求,A收到这个SYN的ACK后也并不会理会它,而B殊不知道,B会一直为这个链接维持着资源,形成资源的浪费,但若是是三次握手,若是第三次握手迟迟不来,服务器便会认为这个SYN是无效的,释放相关资源
成功发起请求并完整走完了上述流程,浏览器能得到服务器发来的数据,那这些数据被放在哪里,它是如何被浏览器处理的?
其实这个问题很简单,在前面成功发起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
数据请求回来之后,浏览器是如何把数据转化成页面的呢?这个过程就涉及到了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 的构建
这样说一遍,仍是在很表面的层次在说渲染这件事,那有没有更深层次的理解呢?能够经过看浏览器源码来进行分析:
大体分为三个步骤:
1.HTMLDocumentParser负责解析html文本为tokens
2.HTMLTreeBuilder对这些tokens分类处理
3.HTMLConstructionSite调用不一样的函数构建DOM树
接下来使用这个html文档来讲明DOM树的构建过程:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <div> <h1 class="title">demo</h1> <input value="hello"> </div> </body> </html>
首先是>>>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: "
接着是>>>>>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 } }
最后,最关键的就是HTMLConstructionSite调用不一样的函数构建DOM树,它根据不一样的节点类型进行不一样的处理
// 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元素的行高处理不同 //标准模式将会让页面遵照文档规定
首先是<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); }
每当遇到一个开标签时,就把它压起来,下一次再遇到一个开标签时,它的父元素就是上一个开标签,借助一个栈创建起了父子关系
第一个闭标签是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/...