本文旨在用最通俗的語言講述最枯燥的基本知識javascript
面試過前端的老鐵都知道,對於前端,面試官喜歡一開始先問些HTML5新增元素啊特性啊,或者是js閉包啊原型啊,或者是css垂直水平居中怎麼實現啊之類的基礎問題,當你能滾瓜爛熟的回答這些以後,面試官臉上會劃過一絲詭異的笑容,而後晴轉多雲,故做深沉的清一下嗓子問:從用戶輸入URL到瀏覽器呈現頁面通過了哪些過程?若是你懂,巴拉巴拉回答了一堆,他又接着問:那網頁具體是如何渲染出來的呢?若是你還懂,又巴拉巴拉的回答了一堆,他還會繼續問:那你有哪些網頁性能優化的經驗呢?當你還能巴拉巴拉的回答了一堆以後,面試官這下內心就有逼數了,轉而去問你一些和技術無關的七大姑八大姨之類的事情,這時候,你就能夠歡呼你的offer基本已經到手了。css
那麼各位問題來了,真正輪到你去面試的時候
你可否很好的回到這些問題呢?html
- 用戶輸入URL回車以後,瀏覽器到底作了啥?
- 頁面渲染的完整流程是怎樣的?
- 前端性能優化有哪些經驗?
若是不能,那咱們往下走:
(有人會疑惑說不是講前端嗎?爲毛要講TCP、DNS這些與前端無關的知識?別慌咯,跟着文章走吧,多學無害!)前端
文章提綱:java
- TCP
- UDP
- 套接字socket
- HTTP協議
- DNS解析
- HTTP請求發起和響應
- 頁面渲染的過程
- 頁面的性能優化
TCP:Transmission Control Protocol, 傳輸控制協議,是一種面向鏈接的、可靠的、基於字節流的傳輸層通訊協議。
說的這麼專業,有啥用呢?
先來舉個栗子吧
還記得小時候咱們作的紙杯電話麼?兩個紙杯用一條繩子連到一塊兒,兩個各拿一個紙杯把線拉直,一個對着紙杯講,一個用耳朵對着紙杯聽。web
這其實就是一種最簡單的鏈接通訊,兩人經過一根線鏈接起來,聲音從這邊的紙杯發出經過線傳輸到另外一個紙杯接收,擴展到如今家家戶戶都有的固定電話也是如此,它的通訊也是創建在雙方可接受而且信任的基礎上進行,如:面試
- A拿起電話,撥通0775-6532122,開始呼叫B
- B聽到電話聲響起,拿起電話,此時A收到B已經拿起電話的聲音
- 雙方開始講話。
回到咱們的tcp協議,其實它和上面所說的電話協議差很少,只不過電話的協議是服務於電話通訊,而tcp是服務於網絡通信的一種協議,相似的,通信雙方創建一次tcp鏈接,也須要通過三個步驟(握手)。編程
- 客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認。
- 服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時本身也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態。
- 客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
上面幾個唧唧歪歪的英文看的有點懵逼,翻譯一下吧:
(你們最好記一下這些狀態碼,在服務器鏈接數的性能優化中會常常用到)後端
SYN:synchronous 創建聯機
ACK:acknowledgement 確認
SYN_SENT:請求鏈接
SYN_RECV:服務端被動打開後,接收到了客戶端的SYN而且發送了ACK時的狀態。再進一步接收到客戶端的ACK就進入ESTABLISHED狀態。瀏覽器
值得注意的是:tcp在握手過程當中並不攜帶數據,(就像你打電話給酒店訂房時,在確認對方是酒店客服人員以前,你也不會立刻把身份證號碼報給他吧?),而是在三次握手完成以後,纔會進行數據傳送。
至於它的應用場景,實際上是根據它自己的特色而定的,好比對網絡通信質量有要求,須要保證數據準確性時,就須要用到TCP協議了,如HTTP、ftp等文件傳輸協議、或一些郵件傳輸協議(SMTP、pop等)
(UDP協議並不是本文須要重點着筆的內容,可是講到TCP了,做爲他的互補兄弟,在此掠過一筆)
UDP :User Datagram Protocol 用戶數據報協議
相比於TCP的面向鏈接須要反覆確認的繁瑣步驟,UDP是一中性格特立獨行而且主觀性超強的非面向鏈接的協議,使用udp協議常常通訊並不須要創建鏈接,它只是負責把數據儘量快的發送出去,簡單粗暴,而且不可靠,而在接收端,UDP把每一個消息斷放入隊列中,接收端程序從隊列中讀取數據。
有人會說,UDP協議這麼不可靠,爲啥還會造出來呢?
話說回來,天底下沒有無用之人,只有你不懂用的人而已,雖然UDP不可靠,可是它的傳輸速度快,效率高,在一些對數據準確性要求不高的場景,UDP就變得頗有用了,好比qq語音、qq視頻。
爲何要說嵌套字?
那是由於就像前面說的,TCP或UDP都是一種協議,也就是計算機網絡通訊中在傳輸層的一種協議,簡單地說,就是一種約定,就像合做雙方的合同同樣,而後合同是死的,只有履行合同纔是實質性的行動,所以不管是TCP仍是UDP要產生做用,都須要有實際的行爲去執行才能體現協議的做用,
那麼,有什麼辦法讓這些協議做用呢?
這就要說到socket了。
socket:也叫嵌套字 ,是一組實現TCP/UDP通訊的接口API,也就是說不管TCP仍是UDP,經過對scoket的編程,均可以實現TCP/UCP通訊,做爲一個通訊鏈的句柄,它包含網絡通訊必備的5種信息:
- 鏈接使用的協議
- 本地主機的IP地址
- 本地進程的協議端口
- 遠地主機的IP地址
- 遠地進程的協議端口
可見,socket包含了通訊本方和對方的ip和端口以及鏈接使用的協議(TCP/UDP)。通訊雙方中的一方(暫稱:客戶端)經過scoket(嵌套字)對另外一方(暫稱:服務端)發起鏈接請求,服務端在網絡上監聽請求,當收到客戶端發來的請求以後,根據socket裏攜帶的信息,定位到客戶端,就相應請求,把socket描述發給客戶端,雙方確認以後鏈接就創建了。
所以套接字之間的鏈接過程有三個步驟:
- 服務器監聽:服務器實時監控網絡狀態等待客戶端發來的鏈接請求
- 客戶端請求:客戶端根據遠程主機服務器的IP地址和協議端口向其發起鏈接請求
- 鏈接確認:服務端收到套接字的鏈接請求以後,就響應請求,把服務端套接字描述發給客戶端,客戶端收到後一旦確認,則雙方創建鏈接,進行數據交互。
一般狀況下socket鏈接就是TCP鏈接,所以socket鏈接一旦創建,通信雙方開始互發數據進行通訊,直到其中一方或雙方斷開鏈接爲止。
socket在即時通信(qq等各類聊天軟件)等應用上應用普遍。
HTTP協議:Hypertext Transfer Protocol 也叫超文本傳送協議 ,它是一種基於TCP/IP協議棧、在表示層和應用層上的協議(TCP在傳輸層的協議),通俗一點說就是:
- TCP/IP是位於傳輸層上的一種協議,用於在網絡中傳輸數據;
- HTTP協議是應用層協議,基於TCP協議,用於包裝數據,程序使用它進行通訊,能夠簡單高效的處理通訊中數據的傳輸和識別處理
而在如今應用很是普遍的HTTP鏈接則是創建在HTTP協議上的、處於應用層中的一種具體應用。
上面說到socket鏈接一旦創建就保持鏈接狀態,而HTTP鏈接則不同,它基於tcp協議的短鏈接,也就是客戶端發起請求,服務器響應請求以後,鏈接就會自動斷開,不會一直保持。
前面講了tcp、udp、http…等等都是爲了講一個具體問題而作的知識點鋪墊,那就是:咱們開發的web應用中請求的發起和響應,是一個怎樣的底層原理。
咱們都知道,web應用絕大部分都是經過HTTP來進行請求的,而URL則是HTTP用來作鏈接創建和傳輸數據的一種具體實現,所以在此要簡單講一下URL。
URL:Uniform Resource Locator 統一資源定位符。說白了就是網絡上用來標識具體資源的一個地址,包含了用戶查找該資源的信息,HTTP使用它來傳輸數據和創建鏈接
一個URL有如下組成部分:
- 協議
- 服務器地址(域名或IP+端口)
- 路徑
- 文件名
好比:https://www.baidu.com/index.html
其中
- https://是一種協議 固然,HTTP也是 ftp也是…
- www.baidu.com是服務器地址,固然你知道百度的IP也能夠,例如我用ping命令獲得百度的ip
14.215.177.39,那麼我能夠用http://14.215.177.39打開百度- index.html包含了路徑和文件名,固然一般index.html是能夠省略的,因此你打開百度時,並無看到這個。
DNS:Domain Name Server,域名服務器。
是進行域名(domain name)和與之相對應的IP地址 (IP address)轉換的服務器。DNS中保存了一張域名(domain name)和與之相對應的IP地址 (IP address)的表,以解析消息的域名。
在平時咱們進行開發時,後端提供的接口地址一般是有IP地址加上端口號(8080什麼鬼的)組成的,可是當咱們把網站發佈出去時,一般都須要把IP改爲用域名。
爲何呢?
你想一想哦,好比谷歌的地址是89.12.21.221:9090,百度的地址是132.21.33.221:8766。。。
這麼一看你根本沒有慾望是記住這些亂七八糟的數字吧?
可是域名就不同了,好比谷歌的google.com,百度的baidu.com 是否是一遍就記住了呢?
因此爲了處理這個問題,就須要用域名去映射IP地址,達到易記易用的目的。
所以,當用戶在瀏覽器輸入https://www.baidu.com回車時,它經歷瞭如下步驟:
- 瀏覽器根據地址去自己緩存中查找dns解析記錄,若是有,則直接返回IP地址,不然瀏覽器會查找操做系統中(hosts文件)是否有該域名的dns解析記錄,若是有則返回。
- 若是瀏覽器緩存和操做系統hosts中均無該域名的dns解析記錄,或者已通過期,此時就會向域名服務器發起請求來解析這個域名。
- 請求會先到LDNS(本地域名服務器),讓它來嘗試解析這個域名,若是LDNS也解析不了,則直接到根域名解析器請求解析
- 根域名服務器給LDNS返回一個所查詢餘的主域名服務器(gTLDServer)地址。
- 此時LDNS再向上一步返回的gTLD服務器發起解析請求。
- gTLD服務器接收到解析請求後查找並返回此域名對應的Name Server域名服務器的地址,這個Name Server一般就是你註冊的域名服務器(好比阿里dns、騰訊dns等)
- Name Server域名服務器會查詢存儲的域名和IP的映射關係表,正常狀況下都根據域名獲得目標IP記錄,連同一個TTL值返回給DNS Server域名服務器
- 返回該域名對應的IP和TTL值,Local DNS Server會緩存這個域名和IP的對應關係,緩存的時間有TTL值控制。
- 把解析的結果返回給用戶,用戶根據TTL值緩存在本地系統緩存中,域名解析過程結束。
若是這篇文章的主題是網絡通訊,那到這裏已經能夠告一段落了,但今天咱們要講的是web應用中請求的發起和響應以及頁面渲染的原理,所以以上只是鋪墊。
在一個web程序開發中,通常都有前端和後端之分,前端負責向後端請求數據和展現頁面,後端負責接收請求和作出響應發回給前端,他們之間的協做的橋樑是什麼呢?
是API
API是什麼?不就是一個URL嗎?
URL又是啥呢?上面說到就是HTTP鏈接的一種具體的載體
所以,
不管對於前端或者是後端,理解HTTP,不管是對自身對編程的理解,仍是和同事協做,都是好處大大的,
下面,根據上面各個知識點的理解,咱們來整理一下並解決一下上面提到的第一個問題:
從用戶輸入URL,到瀏覽器呈現給用戶頁面,通過了什麼過程
- 用戶輸入URL,瀏覽器獲取到URL
- 瀏覽器(應用層)進行DNS解析(若是輸入的是IP地址,此步驟省略)
- 根據解析出的IP地址+端口,瀏覽器(應用層)發起HTTP請求,請求中攜帶(請求頭header(也可細分爲請求行和請求頭)、請求體body),
header包含:
- 請求的方法(get、post、put..)
- 協議(http、https、ftp、sftp…)
- 目標url(具體的請求路徑已經文件名)
- 一些必要信息(緩存、cookie之類)
body包含:
- 請求的內容
- 請求到達傳輸層,tcp協議爲傳輸報文提供可靠的字節流傳輸服務,它經過三次握手等手段來保證傳輸過程當中的安全可靠。經過對大塊數據的分割成一個個報文段的方式提供給大量數據的便攜傳輸。
- 到網絡層, 網絡層經過ARP尋址獲得接收方的Mac地址,IP協議把在傳輸層被分割成一個個數據包傳送接收方。
- 數據到達數據鏈路層,請求階段完成
- 接收方在數據鏈路層收到數據包以後,層層傳遞到應用層,接收方應用程序就得到到請求報文。
- 接收方收到發送方的HTTP請求以後,進行請求文件資源(如HTML頁面)的尋找並響應報文
- 發送方收到響應報文後,若是報文中的狀態碼錶示請求成功,則接受返回的資源(如HTML文件),進行頁面渲染。
當一個請求的發起和響應都完成以後,瀏覽器就會收到響應內容,但瀏覽器收到的是一串串的代碼或URL連接,怎麼把這些代碼轉化成用戶能夠看得懂的界面呈現出來,就是瀏覽器的工做了。
目前市場上的瀏覽器已經不下百種,各個瀏覽器根據內核又能夠分紅幾大類,每一類瀏覽器對頁面的渲染原理和過程有所差別。
但總的來講,各個瀏覽器渲染頁面都基本遵循以下圖的流程:
圖中有幾處英文詞彙可能很差理解,不要緊,先作一下解釋:
- HTML parser:HTML解析器,其本質是將HTML文本解釋成DOM tree。
- CSS parser:CSS解析器,其本質是講DOM中各元素對象加入樣式信息
- JavaScript引擎:專門處理JavaScript腳本的虛擬機,其本質是解析JS代碼而且把邏輯(HTML和CSS的操做)應用到佈局中,從而按程序要的要求呈現相應的結果
- DOM tree:文檔對象模型樹,也就是瀏覽器經過HTMLparser解析HTML頁面生成的HTML樹狀結構以及相應的接口。
- render tree:渲染樹,也就是瀏覽器引擎經過DOM Tree和CSS Rule Tree構建出來的一個樹狀結構,和dom tree不同的是,它只有要最終呈現出來的內容,像或者帶有display:none的節點是不存在render tree中的。
- layout:也叫reflow 重排,渲染中的一種行爲。當rendertree中任一節點的幾何尺寸發生改變了,render tree都會從新佈局。
- repaint:重繪,渲染中的一種行爲。render tree中任一元素樣式屬性(幾何尺寸沒改變)發生改變了,render tree都會從新畫,好比字體顏色、背景等變化。
因此,根據關鍵詞彙的解釋以及順着流程圖的流程,能夠總結出,瀏覽器解析渲染頁面主要包括如下過程:
- 瀏覽器經過HTMLParser根據深度遍歷的原則把HTML解析成DOM Tree。
- 將CSS解析成CSS Rule Tree(CSSOM Tree)。
- 根據DOM樹和CSSOM樹來構造render Tree。
- layout:根據獲得的render tree來計算全部節點在屏幕的位置。
- paint:遍歷render樹,並調用硬件圖形API來繪製每一個節點。
對於頁面渲染基本上這樣就是一個的流程,看完以後,有沒有什麼感受在實際編碼中能夠優化的點呢?沒有吧?由於不少細節都沒有講述,所以爲了找到可優化的點,在此對頁面渲染過程的幾個關鍵步驟作一下陳述:
上面講到,HTML解析是瀏覽器的HTML解析器把HTML解析成dom tree,而在解析過程,瀏覽器根據HTML文件的結構從上到下解析html,HTML元素是以深度優先的方式解析,而script、link、style等標籤會使解析過程產生阻塞,阻塞的狀況有:
- 外部樣式會阻塞內部腳本的執行。
- 外部樣式與外部腳本並行加載,但外部樣式會阻塞外部腳本執行。
- 若是外部腳本帶有async屬性,則外部腳本的加載與執行不受外部樣式影響
- 若是link標籤是動態建立(js生成),無論有無async屬性,都不會阻塞外部腳本的加載與執行。
CSS Parser做用就是將不少個CSS文件中的樣式合併解析出具備樹形結構Style Rules,在對樣式解析的過程當中,默認CSS選擇器是從右往左進行解析的。至於爲何是從右到左,而不是從左到右、也是不會從左到左…
下面舉個栗子來講一下:
假如如今有這樣的一個樣式:
1#parent .ch1 .dh1 {}
2.fh1 .ch1 .dh1{}
3.ah1 .ch1 .eh1 {}
4#parent .fh1 {}
5.ch1 .dh1{}
複製代碼
咱們來比較從左到右和從右到左兩種方式的結果:
- 右邊的tree複雜度要比左邊的低
- 右邊的tree公用樣式重合度比左邊的低
- 右邊的tree從根開始的節點數要比左邊的少
可能光看這幾點沒看出什麼問題,但你要知道:瀏覽器中的css解析器負責css的解析,併爲每一個節點計算出樣式,所以雖然css解析器要作的事情很少,但要每一個節點都要進行遍歷查找計算,計算量極大,所以解析的方式是決定其性能的關鍵點。
就如
1#parant .a{}
2和
3.a{}
複製代碼
估計絕大多數人都會認爲前者要比後者性能更優,其實否則,在解析過程當中
#paran .a{}意味着css解析器要先找到#parent再找到他下面的.a所在節點
然後者能夠直接定位到.a{}所以哪種方式更優,顯而易見。
瀏覽器解析HTML時,當遇到<script>標籤就會當即解析腳本,同時阻塞解析文檔直到腳本執行完畢(你可能問爲何要這樣設計,明顯啊,腳本的執行是改變css和dom,會形成render tree不停的重繪和重排的),而當<script>是引入外部js文件時,會阻塞到js文件下載完成而且執行完成爲止(除非加了defer或者async屬性)。腳本在解析過程當中將對dom或css的操做解析出來加入到DOM Tree和cssom中。
把這些度講完以後,對於性能優化的點,相信你們內心都有點X數了吧,下面簡單總結一下平常開發過程當中經常使用的性能優化的地方:
- 優化選擇器路徑:健全的css選擇器當然是能讓開發看起來更清晰,而後對於css的解析來講倒是個很大的性能問題,所以相比於 .a .b .c{} ,更傾向於你們寫.c{}。
- 壓縮文件:儘量的壓縮你的css文件大小,減小資源下載的負擔。
- 選擇器合併:把有共同的屬性內容的一系列選擇器組合到一塊兒,能壓縮空間和資源開銷
- 精準樣式:儘量減小沒必要要的屬性設置,好比你只要設置{padding-left:10px}的值,那就避免{padding:0 0 0 10px}這樣的寫法
- 雪碧圖:在合理的地方把一些小的圖標合併到一張圖中,這樣全部的圖片只須要一次請求,而後經過定位的方式獲取相應的圖標,這樣能避免一個圖標一次請求的資源浪費。
- 避免通配符:.a .b *{} 像這樣的選擇器,根據從右到左的解析順序在解析過程當中遇到通配符(*)回去遍歷整個dom的,這樣性能問題就大大的了。
- 少用Float:Float在渲染時計算量比較大,儘可能減小使用。
- 0值去單位:對於爲0的值,儘可能不要加單位,增長兼容性
- 儘量把script標籤放到body以後,避免頁面須要等待js執行完成以後dom才能繼續執行,最大程度保證頁面儘快的展現出來。
- 儘量合併script代碼,
- css能幹的事情,儘可能不要用JavaScript來幹。畢竟JavaScript的解析執行過於直接和粗暴,而css效率更高。
- 儘量壓縮的js文件,減小資源下載的負擔
- 儘量避免在js中逐條操做dom樣式,儘量預約義好css樣式,而後經過改變樣式名來修改dom樣式,這樣集中式的操做能減小reflow或repaint的次數。
- 儘量少的在js中建立dom,而是預先埋到HTML中用display:none來隱藏,在js中按需調用,減小js對dom的暴力操做。
- 避免再HTML中直接寫css代碼。
- 使用Viewport加速頁面的渲染。
- 使用語義化標籤,減小css的代碼,增長可讀性和SEO。
- 減小標籤的使用,dom解析是一個大量遍歷的過程,減小無必要的標籤,能下降遍歷的次數。
- 避免src、href等的值爲空。
- 減小dns查詢的次數。
以上就是文章的全部內容,總的來講,入門的文章是領人入門,進階的文章帶人進階,就像Java的書會有入門教程和進階教程同樣,這個文章裏邊寫的大部分知識點都是爲了讓讀者對頁面請求和呈現有一個鋪墊和總體的認知,因爲涉及的知識點過多,每一個知識點拎出來均可以寫一本書,因此你們把本文做爲一個引路文,須要對某個知識點進行深刻研究時再找相關書籍研究,不喜勿噴。
以爲本文對你有幫助?請分享給更多人
關注「編程無界」,提高裝逼技能![]()