一篇文章搞定前端面試

jaybril發表於2018-10-01

本文旨在用最通俗的語言講述最枯燥的基本知識

面試過前端的老鐵都知道,對於前端,面試官喜歡一開始先問些HTML5新增元素啊特性啊,或者是js閉包啊原型啊,或者是css垂直水平居中怎麼實現啊之類的基礎問題,當你能倒背如流的回答這些之後,面試官臉上會劃過一絲詭異的笑容,然後晴轉多雲,故作深沉的清一下嗓子問:從使用者輸入URL到瀏覽器呈現頁面經過了哪些過程?如果你懂,巴拉巴拉回答了一堆,他又接著問:那網頁具體是如何渲染出來的呢?如果你還懂,又巴拉巴拉的回答了一堆,他還會繼續問:那你有哪些網頁效能優化的經驗呢?當你還能巴拉巴拉的回答了一堆之後,面試官這下心裡就有逼數了,轉而去問你一些和技術無關的七大姑八大姨之類的事情,這時候,你就可以歡呼你的offer基本已經到手了。

那麼各位問題來了,真正輪到你去面試的時候
你能否很好的回到這些問題呢?

  1. 使用者輸入URL回車之後,瀏覽器到底做了啥?
  2. 頁面渲染的完整流程是怎樣的?
  3. 前端效能優化有哪些經驗?

如果不能,那我們往下走:
(有人會疑惑說不是講前端嗎?為毛要講TCP、DNS這些與前端無關的知識?別慌咯,跟著文章走吧,多學無害!)

文章提綱:

  1. TCP
  2. UDP
  3. 套接字socket
  4. HTTP協議
  5. DNS解析
  6. HTTP請求發起和響應
  7. 頁面渲染的過程
  8. 頁面的效能優化

TCP連線

TCP:Transmission Control Protocol, 傳輸控制協議,是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協議。
說的這麼專業,有啥用呢?
先來舉個例子吧
還記得小時候我們做的紙杯電話麼?兩個紙杯用一條繩子連到一起,兩個各拿一個紙杯把線拉直,一個對著紙杯講,一個用耳朵對著紙杯聽。

圖片描述

這其實就是一種最簡單的連線通訊,兩人通過一根線連線起來,聲音從這邊的紙杯發出通過線傳輸到另一個紙杯接收,擴充套件到現在家家戶戶都有的固定電話也是如此,它的通訊也是建立在雙方可接受並且信任的基礎上進行,如:

  1. A拿起電話,撥通0775-6532122,開始呼叫B
  2. B聽到電話聲響起,拿起電話,此時A收到B已經拿起電話的聲音
  3. 雙方開始講話。

回到我們的tcp協議,其實它和上面所說的電話協議差不多,只不過電話的協議是服務於電話通訊,而tcp是服務於網路通訊的一種協議,類似的,通訊雙方建立一次tcp連線,也需要經過三個步驟(握手)。

  1. 客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SEND狀態,等待伺服器確認。
  2. 伺服器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態。
  3. 客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED狀態,完成三次握手。

tcp%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.png

上面幾個唧唧歪歪的英文看的有點懵逼,翻譯一下吧:
(大家最好記一下這些狀態碼,在伺服器連線數的效能優化中會經常用到)

SYN:synchronous 建立聯機
ACK:acknowledgement 確認
SYN_SENT:請求連線
SYN_RECV:服務端被動開啟後,接收到了客戶端的SYN並且傳送了ACK時的狀態。再進一步接收到客戶端的ACK就進入ESTABLISHED狀態。

值得注意的是:tcp在握手過程中並不攜帶資料,(就像你打電話給酒店訂房時,在確認對方是酒店客服人員之前,你也不會馬上把身份證號碼報給他吧?),而是在三次握手完成之後,才會進行資料傳送

至於它的應用場景,其實是根據它本身的特點而定的,比如對網路通訊質量有要求,需要保證資料準確性時,就需要用到TCP協議了,如HTTP、ftp等檔案傳輸協議、或一些郵件傳輸協議(SMTP、pop等)

UDP協議

(UDP協議並非本文需要重點著筆的內容,但是講到TCP了,作為他的互補兄弟,在此掠過一筆)

UDP :User Datagram Protocol 使用者資料包協議
相比於TCP的面向連線需要反覆確認的繁瑣步驟,UDP是一中性格特立獨行並且主觀性超強的非面向連線的協議,使用udp協議經常通訊並不需要建立連線,它只是負責把資料儘可能快的傳送出去,簡單粗暴,並且不可靠,而在接收端,UDP把每個訊息斷放入佇列中,接收端程式從佇列中讀取資料。

有人會說,UDP協議這麼不可靠,為啥還會造出來呢?
話說回來,天底下沒有無用之人,只有你不懂用的人而已,雖然UDP不可靠,但是它的傳輸速度快,效率高,在一些對資料準確性要求不高的場景,UDP就變得很有用了,比如qq語音、qq視訊。

套接字socket

為什麼要說巢狀字?
那是因為就像前面說的,TCP或UDP都是一種協議,也就是計算機網路通訊中在傳輸層的一種協議,簡單地說,就是一種約定,就像合作雙方的合同一樣,然後合同是死的,只有履行合同才是實質性的行動,因此無論是TCP還是UDP要產生作用,都需要有實際的行為去執行才能體現協議的作用,
那麼,有什麼辦法讓這些協議作用呢?
這就要說到socket了。

socket:也叫巢狀字 ,是一組實現TCP/UDP通訊的介面API,也就是說無論TCP還是UDP,通過對scoket的程式設計,都可以實現TCP/UCP通訊,作為一個通訊鏈的控制程式碼,它包含網路通訊必備的5種資訊:

  1. 連線使用的協議
  2. 本地主機的IP地址
  3. 本地程式的協議埠
  4. 遠地主機的IP地址
  5. 遠地程式的協議埠

可見,socket包含了通訊本方和對方的ip和埠以及連線使用的協議(TCP/UDP)。通訊雙方中的一方(暫稱:客戶端)通過scoket(巢狀字)對另一方(暫稱:服務端)發起連線請求,服務端在網路上監聽請求,當收到客戶端發來的請求之後,根據socket裡攜帶的資訊,定位到客戶端,就相應請求,把socket描述發給客戶端,雙方確認之後連線就建立了。
因此套接字之間的連線過程有三個步驟:

  1. 伺服器監聽:伺服器實時監控網路狀態等待客戶端發來的連線請求
  2. 客戶端請求:客戶端根據遠端主機伺服器的IP地址和協議埠向其發起連線請求
  3. 連線確認:服務端收到套接字的連線請求之後,就響應請求,把服務端套接字描述發給客戶端,客戶端收到後一旦確認,則雙方建立連線,進行資料互動。

通常情況下socket連線就是TCP連線,因此socket連線一旦建立,通訊雙方開始互發資料進行通訊,直到其中一方或雙方斷開連線為止。

socket在即時通訊(qq等各種聊天軟體)等應用上應用廣泛。

HTTP協議

HTTP協議:Hypertext Transfer Protocol 也叫超文字傳送協議 ,它是一種基於TCP/IP協議棧、在表示層和應用層上的協議(TCP在傳輸層的協議),通俗一點說就是:

  • TCP/IP是位於傳輸層上的一種協議,用於在網路中傳輸資料;
  • HTTP協議是應用層協議,基於TCP協議,用於包裝資料,程式使用它進行通訊,可以簡單高效的處理通訊中資料的傳輸和識別處理

而在現在應用非常廣泛的HTTP連線則是建立在HTTP協議上的、處於應用層中的一種具體應用。
上面說到socket連線一旦建立就保持連線狀態,而HTTP連線則不一樣,它基於tcp協議的短連線,也就是客戶端發起請求,伺服器響應請求之後,連線就會自動斷開,不會一直保持。

URL

前面講了tcp、udp、http…等等都是為了講一個具體問題而做的知識點鋪墊,那就是:我們開發的web應用中請求的發起和響應,是一個怎樣的底層原理。
我們都知道,web應用絕大部分都是通過HTTP來進行請求的,而URL則是HTTP用來做連線建立和傳輸資料的一種具體實現,因此在此要簡單講一下URL。

URL:Uniform Resource Locator 統一資源定位符。說白了就是網路上用來標識具體資源的一個地址,包含了使用者查詢該資源的資訊,HTTP使用它來傳輸資料和建立連線
一個URL有以下組成部分:

  1. 協議
  2. 伺服器地址(域名或IP+埠)
  3. 路徑
  4. 檔名

比如:https://www.baidu.com/index.html
其中

  1. https://是一種協議 當然,HTTP也是 ftp也是…
  2. www.baidu.com是伺服器地址,當然你知道百度的IP也可以,例如我用ping命令得到百度的ip

14.215.177.39,那麼我可以用http://14.215.177.39開啟百度

  1. index.html包含了路徑和檔名,當然通常index.html是可以省略的,所以你開啟百度時,並沒有看到這個。

DNS

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回車時,它經歷了以下步驟:

  1. 瀏覽器根據地址去本身快取中查詢dns解析記錄,如果有,則直接返回IP地址,否則瀏覽器會查詢作業系統中(hosts檔案)是否有該域名的dns解析記錄,如果有則返回。
  2. 如果瀏覽器快取和作業系統hosts中均無該域名的dns解析記錄,或者已經過期,此時就會向域名伺服器發起請求來解析這個域名。
  3. 請求會先到LDNS(本地域名伺服器),讓它來嘗試解析這個域名,如果LDNS也解析不了,則直接到根域名解析器請求解析
  4. 根域名伺服器給LDNS返回一個所查詢餘的主域名伺服器(gTLDServer)地址。
  5. 此時LDNS再向上一步返回的gTLD伺服器發起解析請求。
  6. gTLD伺服器接收到解析請求後查詢並返回此域名對應的Name Server域名伺服器的地址,這個Name Server通常就是你註冊的域名伺服器(比如阿里dns、騰訊dns等)
  7. Name Server域名伺服器會查詢儲存的域名和IP的對映關係表,正常情況下都根據域名得到目標IP記錄,連同一個TTL值返回給DNS Server域名伺服器
  8. 返回該域名對應的IP和TTL值,Local DNS Server會快取這個域名和IP的對應關係,快取的時間有TTL值控制。
  9. 把解析的結果返回給使用者,使用者根據TTL值快取在本地系統快取中,域名解析過程結束。

HTTP請求發起和響應

如果這篇文章的主題是網路通訊,那到這裡已經可以告一段落了,但今天我們要講的是web應用中請求的發起和響應以及頁面渲染的原理,因此以上只是鋪墊。
在一個web程式開發中,一般都有前端和後端之分,前端負責向後端請求資料和展示頁面,後端負責接收請求和做出響應發回給前端,他們之間的協作的橋樑是什麼呢?
是API
API是什麼?不就是一個URL嗎?
URL又是啥呢?上面說到就是HTTP連線的一種具體的載體
因此,
無論對於前端或者是後端,理解HTTP,無論是對自身對程式設計的理解,還是和同事協作,都是好處大大的,
下面,根據上面各個知識點的理解,我們來整理一下並解決一下上面提到的第一個問題:
從使用者輸入URL,到瀏覽器呈現給使用者頁面,經過了什麼過程

  1. 使用者輸入URL,瀏覽器獲取到URL
  2. 瀏覽器(應用層)進行DNS解析(如果輸入的是IP地址,此步驟省略)
  3. 根據解析出的IP地址+埠,瀏覽器(應用層)發起HTTP請求,請求中攜帶(請求頭header(也可細分為請求行和請求頭)、請求體body),

header包含:

  1. 請求的方法(get、post、put..)
  2. 協議(http、https、ftp、sftp…)
  3. 目標url(具體的請求路徑已經檔名)
  4. 一些必要資訊(快取、cookie之類)

body包含:

  1. 請求的內容
  1. 請求到達傳輸層,tcp協議為傳輸報文提供可靠的位元組流傳輸服務,它通過三次握手等手段來保證傳輸過程中的安全可靠。通過對大塊資料的分割成一個個報文段的方式提供給大量資料的便攜傳輸。
  2. 到網路層, 網路層通過ARP定址得到接收方的Mac地址,IP協議把在傳輸層被分割成一個個資料包傳送接收方。
  3. 資料到達資料鏈路層,請求階段完成
  4. 接收方在資料鏈路層收到資料包之後,層層傳遞到應用層,接收方應用程式就獲得到請求報文。
  5. 接收方收到傳送方的HTTP請求之後,進行請求檔案資源(如HTML頁面)的尋找並響應報文
  6. 傳送方收到響應報文後,如果報文中的狀態碼錶示請求成功,則接受返回的資源(如HTML檔案),進行頁面渲染。

頁面的渲染

當一個請求的發起和響應都完成之後,瀏覽器就會收到響應內容,但瀏覽器收到的是一串串的程式碼或URL連結,怎麼把這些程式碼轉化成使用者可以看得懂的介面呈現出來,就是瀏覽器的工作了。
目前市場上的瀏覽器已經不下百種,各個瀏覽器根據核心又可以分成幾大類,每一類瀏覽器對頁面的渲染原理和過程有所差異。

但總的來說,各個瀏覽器渲染頁面都基本遵循如下圖的流程:

圖中有幾處英文詞彙可能不好理解,沒關係,先做一下解釋:

  1. HTML parser:HTML解析器,其本質是將HTML文字解釋成DOM tree。
  2. CSS parser:CSS解析器,其本質是講DOM中各元素物件加入樣式資訊
  3. JavaScript引擎:專門處理JavaScript指令碼的虛擬機器,其本質是解析JS程式碼並且把邏輯(HTML和CSS的操作)應用到佈局中,從而按程式要的要求呈現相應的結果
  4. DOM tree:文件物件模型樹,也就是瀏覽器通過HTMLparser解析HTML頁面生成的HTML樹狀結構以及相應的介面。
  5. render tree:渲染樹,也就是瀏覽器引擎通過DOM Tree和CSS Rule Tree構建出來的一個樹狀結構,和dom tree不一樣的是,它只有要最終呈現出來的內容,像<head>或者帶有display:none的節點是不存在render tree中的。
  6. layout:也叫reflow 重排,渲染中的一種行為。當rendertree中任一節點的幾何尺寸發生改變了,render tree都會重新佈局。
  7. repaint:重繪,渲染中的一種行為。render tree中任一元素樣式屬性(幾何尺寸沒改變)發生改變了,render tree都會重新畫,比如字型顏色、背景等變化。

所以,根據關鍵詞彙的解釋以及順著流程圖的流程,可以總結出,瀏覽器解析渲染頁面主要包括以下過程:

  1. 瀏覽器通過HTMLParser根據深度遍歷的原則把HTML解析成DOM Tree。
  2. 將CSS解析成CSS Rule Tree(CSSOM Tree)。
  3. 根據DOM樹和CSSOM樹來構造render Tree。
  4. layout:根據得到的render tree來計算所有節點在螢幕的位置。
  5. paint:遍歷render樹,並呼叫硬體圖形API來繪製每個節點。

前端效能優化

對於頁面渲染基本上這樣就是一個的流程,看完之後,有沒有什麼感覺在實際編碼中可以優化的點呢?沒有吧?因為很多細節都沒有講述,因此為了找到可優化的點,在此對頁面渲染過程的幾個關鍵步驟做一下陳述:

1. HTML解析:

上面講到,HTML解析是瀏覽器的HTML解析器把HTML解析成dom tree,而在解析過程,瀏覽器根據HTML檔案的結構從上到下解析html,HTML元素是以深度優先的方式解析,而script、link、style等標籤會使解析過程產生阻塞,阻塞的情況有:

  1. 外部樣式會阻塞內部指令碼的執行。
  2. 外部樣式與外部指令碼並行載入,但外部樣式會阻塞外部指令碼執行。
  3. 如果外部指令碼帶有async屬性,則外部指令碼的載入與執行不受外部樣式影響
  4. 如果link標籤是動態建立(js生成),不管有無async屬性,都不會阻塞外部指令碼的載入與執行。
2. CSS解析:

CSS Parser作用就是將很多個CSS檔案中的樣式合併解析出具有樹形結構Style Rules,在對樣式解析的過程中,預設CSS選擇器是從右往左進行解析的。至於為什麼是從右到左,而不是從左到右、也是不會從左到左…
下面舉個例子來說一下:
假如現在有這樣的一個樣式:

#parent .ch1 .dh1 {}
.fh1 .ch1 .dh1{}
.ah1 .ch1 .eh1 {}
#parent .fh1 {}
.ch1 .dh1{}

我們來比較從左到右和從右到左兩種方式的結果:

從兩個圖的比較就可以看幾點:

  1. 右邊的tree複雜度要比左邊的低
  2. 右邊的tree公用樣式重合度比左邊的低
  3. 右邊的tree從根開始的節點數要比左邊的少

可能光看這幾點沒看出什麼問題,但你要知道:瀏覽器中的css解析器負責css的解析,併為每個節點計算出樣式,因此雖然css解析器要做的事情不多,但要每個節點都要進行遍歷查詢計算,計算量極大,因此解析的方式是決定其效能的關鍵點。
就如

#parant .a{}
和
.a{}

估計絕大多數人都會認為前者要比後者效能更優,其實不然,在解析過程中
#paran .a{}意味著css解析器要先找到#parent再找到他下面的.a所在節點
而後者可以直接定位到.a{}因此哪一種方式更優,顯而易見。

3. 指令碼執行:

瀏覽器解析HTML時,當遇到<script>標籤就會立即解析指令碼,同時阻塞解析文件直到指令碼執行完畢(你可能問為什麼要這樣設計,明顯啊,指令碼的執行是改變css和dom,會造成render tree不停的重繪和重排的),而當<script>是引入外部js檔案時,會阻塞到js檔案下載完成並且執行完成為止(除非加了defer或者async屬性)。指令碼在解析過程中將對dom或css的操作解析出來加入到DOM Tree和cssom中。


效能優化

把這些度講完之後,對於效能優化的點,相信大家心裡都有點X數了吧,下面簡單總結一下日常開發過程中常用的效能優化的地方:

1.對於css:
  1. 優化選擇器路徑:健全的css選擇器固然是能讓開發看起來更清晰,然後對於css的解析來說卻是個很大的效能問題,因此相比於 .a .b .c{} ,更傾向於大家寫.c{}。
  2. 壓縮檔案:儘可能的壓縮你的css檔案大小,減少資源下載的負擔。
  3. 選擇器合併:把有共同的屬性內容的一系列選擇器組合到一起,能壓縮空間和資源開銷
  4. 精準樣式:儘可能減少不必要的屬性設定,比如你只要設定{padding-left:10px}的值,那就避免{padding:0 0 0 10px}這樣的寫法
  5. 雪碧圖:在合理的地方把一些小的圖示合併到一張圖中,這樣所有的圖片只需要一次請求,然後通過定位的方式獲取相應的圖示,這樣能避免一個圖示一次請求的資源浪費。
  6. 避免萬用字元:.a .b *{} 像這樣的選擇器,根據從右到左的解析順序在解析過程中遇到萬用字元(*)回去遍歷整個dom的,這樣效能問題就大大的了。
  7. 少用Float:Float在渲染時計算量比較大,儘量減少使用。
  8. 0值去單位:對於為0的值,儘量不要加單位,增加相容性
2.對於JavaScript:
  1. 儘可能把script標籤放到body之後,避免頁面需要等待js執行完成之後dom才能繼續執行,最大程度保證頁面儘快的展示出來。
  2. 儘可能合併script程式碼,
  3. css能幹的事情,儘量不要用JavaScript來幹。畢竟JavaScript的解析執行過於直接和粗暴,而css效率更高。
  4. 儘可能壓縮的js檔案,減少資源下載的負擔
  5. 儘可能避免在js中逐條操作dom樣式,儘可能預定義好css樣式,然後通過改變樣式名來修改dom樣式,這樣集中式的操作能減少reflow或repaint的次數。
  6. 儘可能少的在js中建立dom,而是預先埋到HTML中用display:none來隱藏,在js中按需呼叫,減少js對dom的暴力操作。
3. 對於HTML:
  1. 避免再HTML中直接寫css程式碼。
  2. 使用Viewport加速頁面的渲染。
  3. 使用語義化標籤,減少css的程式碼,增加可讀性和SEO。
  4. 減少標籤的使用,dom解析是一個大量遍歷的過程,減少無必要的標籤,能降低遍歷的次數。
  5. 避免src、href等的值為空。
  6. 減少dns查詢的次數。

以上就是文章的所有內容,總的來說,入門的文章是領人入門,進階的文章帶人進階,就像Java的書會有入門教程和進階教程一樣,這個文章裡邊寫的大部分知識點都是為了讓讀者對頁面請求和呈現有一個鋪墊和整體的認知,由於涉及的知識點過多,每個知識點拎出來都可以寫一本書,所以大家把本文作為一個引路文,需要對某個知識點進行深入研究時再找相關書籍研究,不喜勿噴。


覺得本文對你有幫助?請分享給更多人
關注「程式設計無界」,提升裝逼技能

相關文章