前言
見解有限,如有描述不當之處,請幫忙指出,如有錯誤,會及時修正。
為什麼要梳理這篇文章?
最近恰好被問到這方面的問題,嘗試整理後發現,這道題的覆蓋面可以非常廣,很適合作為一道承載知識體系的題目。
關於這道題目的吐槽暫且不提(這是一道被提到無數次的題,得到不少人的贊同,也被很多人反感),本文的目的是如何藉助這道題梳理自己的前端知識體系!
竊認為,每一個前端人員,如果要往更高階發展,必然會將自己的知識體系梳理一遍,沒有牢固的知識體系,無法往更高處走!
**展現形式:**本文並不是將所有的知識點列一遍,而是偏向於分析+梳理
內容:在本文中只會梳理一些比較重要的前端向知識點,其它的可能會被省略
目標:本文的目標是梳理一個較為完整的前端向知識體系
本文是個人階段性梳理知識體系的成果,然後加以修繕後釋出成文章,因此並不確保適用於所有人員,但是,個人認為本文還是有一定參考價值的
另外,如有不同見解,可以一起討論
----------超長文預警,需要花費大量時間。----------
本文適合有一定經驗的前端人員,新手請規避。
本文內容超多,建議先了解主幹,然後分成多批次閱讀。
本文是前端向,以前端領域的知識為重點
大綱
-
對知識體系進行一次預評級
-
為什麼說知識體系如此重要?
-
梳理主幹流程
-
從瀏覽器接收url到開啟網路請求執行緒
-
多程式的瀏覽器
-
多執行緒的瀏覽器核心
-
解析URL
-
網路請求都是單獨的執行緒
-
更多
-
-
開啟網路執行緒到發出一個完整的http請求
-
DNS查詢得到IP
-
tcp/ip請求
-
五層因特網協議棧
-
-
從伺服器接收到請求到對應後臺接收到請求
-
負載均衡
-
後臺的處理
-
-
後臺和前臺的http互動
-
http報文結構
-
cookie以及優化
-
gzip壓縮
-
長連線與短連線
-
http 2.0
-
https
-
-
單獨拎出來的快取問題,http的快取
-
強快取與弱快取
-
快取頭部簡述
-
頭部的區別
-
-
解析頁面流程
-
流程簡述
-
HTML解析,構建DOM
-
生成CSS規則
-
構建渲染樹
-
渲染
-
簡單層與複合層
-
Chrome中的除錯
-
資源外鏈的下載
-
loaded和domcontentloaded
-
-
CSS的視覺化格式模型
-
包含塊(Containing Block)
-
控制框(Controlling Box)
-
BFC(Block Formatting Context)
-
IFC(Inline Formatting Context)
-
其它
-
-
JS引擎解析過程
-
JS的解釋階段
-
JS的預處理階段
-
JS的執行階段
-
回收機制
-
-
其它
-
總結
對知識體系進行一次預評級
看到這道題目,不借助搜尋引擎,自己的心裡是否有一個答案?
這裡,以目前的經驗(瞭解過一些處於不同階段的相關前端人員的情況),大概有以下幾種情況:(以下都是以點見面,實際上不同階段人員一般都會有其它的隱藏知識點的)
level1:
完全沒什麼概念的,支支吾吾的回答,一般就是這種水平(大致形象點描述):
- 瀏覽器發起請求,服務端返回資料,然後前端解析成網頁,執行指令碼。。。
這類人員一般都是:
-
萌新(剛接觸前端的,包括0-6個月都有可能有這種回答)
-
沉澱人員(就是那種可能已經接觸了前端幾年,但是仍然處於初級階段的那種。。。)
當然了,後者一般還會偶爾提下**http
、後臺
、瀏覽器渲染
,js引擎
**等等關鍵字,但基本都是一詳細的問就不知道了。。。
level2:
已經有初步概念,但是可能沒有完整梳理過,導致無法形成一個完整的體系,或者是很多細節都不會展開,大概是這樣子的:(可能符合若干條)
-
知道瀏覽器輸入url後會有http請求這個概念
-
有後臺這個概念,大致知道前後端的互動,知道前後端只要靠http報文通訊
-
知道瀏覽器接收到資料後會進行解析,有一定概念,但是具體流程不熟悉(如render樹構建流程,layout、paint,複合層與簡單層,常用優化方案等不是很熟悉)
-
對於js引擎的解析流程有一定概念,但是細節不熟悉(如具體的形參,函式,變數提升,執行上下文以及VO、AO、作用域鏈,回收機制等概念不是很熟悉)
-
如可能知道一些http規範初步概念,但是不熟悉(如http報文結構,常用頭部,快取機制,http2.0,https等特性,跨域與web安全等不是很熟悉)
到這裡,看到這上面一大堆的概念後,心裡應該也會有點底了。。。
實際上,大部分的前端人員可能都處於level2,但是,跳出這個階段並不容易,一般需要積累,不斷學習,才能水到渠成
這類人員一般都是:
-
工作1-3年左右的普通人員(佔大多數,而且大多數人員工作3年左右並沒有實質上的提升)
-
工作3年以上的老人(這部分人大多都業務十分嫻熟,一個當好幾個用,但是,基礎比較薄弱,可能沒有嘗試寫過框架、元件、腳手架等)
大部分的初中級都陷在這個階段,如果要突破,不斷學習,積累,自然能水到渠成,打通任督二脈
level3:
基本能到這一步的,不是高階就是接近高階,因為很多概念並不是靠背就能理解的,而要理解這麼多,需形成體系,一般都需要積累,非一日之功。
一般包括什麼樣的回答呢?(這裡就以自己的簡略回答進行舉例),一般這個階段的人員都會符合若干條(不一定全部,當然可能還有些是這裡遺漏的):
-
首先略去那些鍵盤輸入、和作業系統互動、以及螢幕顯示原理、網路卡等硬體互動之類的(前端向中,很多硬體原理暫時略去。。。)
-
對瀏覽器模型有整體概念,知道瀏覽器是多程式的,瀏覽器核心是多執行緒的,清楚程式與執行緒之間得區別,以及輸入url後會開一個新的網路執行緒
-
對從開啟網路執行緒到發出一個完整的http請求中間的過程有所瞭解(如dns查詢,tcp/ip連結,五層因特網協議棧等等,以及一些優化方案,如
dns-prefetch
) -
對從伺服器接收到請求到對應後臺接收到請求有一定了解(如負載均衡,安全攔截以及後臺程式碼處理等)
-
對後臺和前臺的http互動熟悉(包括http報文結構,場景頭部,cookie,跨域,web安全,http快取,http2.0,https等)
-
對瀏覽器接收到http資料包後的解析流程熟悉(包括解析html,詞法分析然後解析成dom樹、解析css生成css規則樹、合併成render樹,然後layout、painting渲染、裡面可能還包括複合圖層的合成、GPU繪製、外鏈處理、載入順序等)
-
對JS引擎解析過程熟悉(包括JS的解釋,預處理,執行上下文,VO,作用域鏈,this,回收機制等)
可以看到,上述包括了一大堆的概念,僅僅是偏前端向,而且沒有詳細展開,就已經如此之多的概念了,所以,個人認為如果沒有自己的見解,沒有形成自己的知識體系,僅僅是看看,背背是沒用的,過一段時間就會忘光了。
再說下一般這個階段的都可能是什麼樣的人吧。(不一定準確,這裡主要是靠少部分現實以及大部分推測得出)
-
工作2年以上的前端(基本上如果按正常進度的話,至少接觸前端兩年左右才會開始走向高階,當然,現在很多都是上學時就開始學了的,還有部分是天賦異稟,不好預估。。。)
-
或者是已經十分熟悉其它某門語言,再轉前端的人(基本上是很快就可以將前端水準提升上去)
一般符合這個條件的都會有各種隱藏屬性(如看過各大框架、元件的原始碼,寫過自己的元件、框架、腳手架,做過大型專案,整理過若干精品博文等)
level4:
由於本人層次尚未達到,所以大致說下自己的見解吧。
一般這個層次,很多大佬都並不僅僅是某個技術棧了,而是成為了技術專家,技術leader之類的角色。所以僅僅是回答某個技術問題已經無法看出水準了, 可能更多的要看架構,整體把控,大型工程構建能力等等
不過,對於某些執著於技術的大佬,大概會有一些回答吧:(猜的)
- 從鍵盤談起到系統互動,從瀏覽器到CPU,從排程機制到系統核心,從資料請求到二進位制、彙編,從GPU繪圖到LCD顯示,然後再分析系統底層的程式、記憶體等等
總之,從軟體到硬體,到材料,到分子,原子,量子,薛定諤的貓,人類起源,宇宙大爆炸,平行宇宙?感覺都毫無違和感。。。
這點可以參考下本題的原始出處:
為什麼說知識體系如此重要?
為什麼說知識體系如此重要呢?這裡舉幾個例子
假設有被問到這樣一道題目(隨意想到的一個):
- 如何理解
getComputedStyle
在尚未梳理知識體系前,大概會這樣回答:
-
普通版本:
getComputedStyle
會獲取當前元素所有最終使用的CSS屬性值(最終計算後的結果),通過window.getComputedStyle
等價於document.defaultView.getComputedStyle
呼叫 -
詳細版本:
window.getComputedStyle(elem, null).getPropertyValue("height")
可能的值為100px
,而且,就算是css上寫的是inherit
,getComputedStyle
也會把它最終計算出來的。不過注意,如果元素的背景色透明,那麼getComputedStyle
獲取出來的就是透明的這個背景(因為透明本身也是有效的),而不會是父節點的背景。所以它不一定是最終顯示的顏色。
就這個API來說,上述的回答已經比較全面了。
但是,其實它是可以繼續延伸的。
譬如現在會這樣回答:
-
getComputedStyle
會獲取當前元素所有最終使用的CSS屬性值,window.
和document.defaultView.
等價... -
getComputedStyle
會引起迴流,因為它需要獲取祖先節點的一些資訊進行計算(譬如寬高等),所以用的時候慎用,迴流會引起效能問題。然後合適的話會將話題引導迴流,重繪,瀏覽器渲染原理等等。當然也可以列舉一些其它會引發迴流的操作,如offsetXXX
,scrollXXX
,clientXXX
,currentStyle
等等
再舉一個例子:
visibility: hidden
和display: none
的區別
可以如下回答:
-
普通回答,一個隱藏,但佔據位置,一個隱藏,不佔據位置
-
進一步,
display
由於隱藏後不佔據位置,所以造成了dom樹的改變,會引發迴流,代價較大 -
再進一步,當一個頁面某個元素經常需要切換
display
時如何優化,一般會用複合層優化,或者要求低一點用absolute
讓其脫離普通文件流也行。然後可以將話題引到普通文件流,absolute
文件流,複合圖層的區別, -
再進一步可以描述下瀏覽器渲染原理以及複合圖層和普通圖層的繪製區別(複合圖層單獨分配資源,獨立繪製,效能提升,但是不能過多,還有隱式合成等等)
上面這些大概就是知識系統化後的回答,會更全面,容易由淺入深,而且一有機會就可以往更底層挖
前端向知識的重點
此部分的內容是站在個人視角分析的,並不是說就一定是正確答案
首先明確,計算機方面的知識是可以無窮無盡的挖的,而本文的重點是梳理前端向的重點知識
對於前端向(這裡可能沒有提到node.js
之類的,更多的是指客戶端前端),這裡將知識點按重要程度劃分成以下幾大類:
-
核心知識,必須掌握的,也是最基礎的,譬如瀏覽器模型,渲染原理,JS解析過程,JS執行機制等,作為骨架來承載知識體系
-
重點知識,往往每一塊都是一個知識點,而且這些知識點都很重要,譬如http相關,web安全相關,跨域處理等
-
擴充知識,這一塊可能更多的是瞭解,稍微實踐過,但是認識上可能沒有上面那麼深刻,譬如五層因特網協議棧,hybrid模式,移動原生開發,後臺相關等等(當然,在不同領域,可能有某些知識就上升到重點知識層次了,譬如hybrid開發時,懂原生開發是很重要的)
為什麼要按上面這種方式劃分?
這大概與個人的技術成長有關。
記得最開始學前端知識時,是一點一點的積累,一個知識點一個知識點的攻克。
就這樣,雖然在很長一段時間內積累了不少的知識,但是,總是無法將它串聯到一起。每次梳理時都是很分散的,無法保持思路連貫性。
直到後來,在將瀏覽器渲染原理、JS執行機制、JS引擎解析流程梳理一遍後,感覺就跟打通了任督二脈一樣,有了一個整體的架構,以前的知識點都連貫起來了。
梳理出了一個知識體系,以後就算再學新的知識,也會盡量往這個體系上靠攏,環環相扣,更容易理解,也更不容易遺忘
梳理主幹流程
回到這道題上,如何回答呢?先梳理一個骨架
知識體系中,最重要的是骨架,脈絡。有了骨架後,才方便填充細節。所以,先梳理下主幹流程:
1. 從瀏覽器接收url到開啟網路請求執行緒(這一部分可以展開瀏覽器的機制以及程式與執行緒之間的關係)
2. 開啟網路執行緒到發出一個完整的http請求(這一部分涉及到dns查詢,tcp/ip請求,五層因特網協議棧等知識)
3. 從伺服器接收到請求到對應後臺接收到請求(這一部分可能涉及到負載均衡,安全攔截以及後臺內部的處理等等)
4. 後臺和前臺的http互動(這一部分包括http頭部、響應碼、報文結構、cookie等知識,可以提下靜態資源的cookie優化,以及編碼解碼,如gzip壓縮等)
5. 單獨拎出來的快取問題,http的快取(這部分包括http快取頭部,etag,catch-control等)
6. 瀏覽器接收到http資料包後的解析流程(解析html-詞法分析然後解析成dom樹、解析css生成css規則樹、合併成render樹,然後layout、painting渲染、複合圖層的合成、GPU繪製、外鏈資源的處理、loaded和domcontentloaded等)
7. CSS的視覺化格式模型(元素的渲染規則,如包含塊,控制框,BFC,IFC等概念)
8. JS引擎解析過程(JS的解釋階段,預處理階段,執行階段生成執行上下文,VO,作用域鏈、回收機制等等)
9. 其它(可以擴充不同的知識模組,如跨域,web安全,hybrid模式等等內容)
複製程式碼
梳理出主幹骨架,然後就需要往骨架上填充細節內容
從瀏覽器接收url到開啟網路請求執行緒
這一部分展開的內容是:瀏覽器程式/執行緒模型,JS的執行機制
多程式的瀏覽器
瀏覽器是多程式的,有一個主控程式,以及每一個tab頁面都會新開一個程式(某些情況下多個tab會合並程式)
程式可能包括主控程式,外掛程式,GPU,tab頁(瀏覽器核心)等等
-
Browser程式:瀏覽器的主程式(負責協調、主控),只有一個
-
第三方外掛程式:每種型別的外掛對應一個程式,僅當使用該外掛時才建立
-
GPU程式:最多一個,用於3D繪製
-
瀏覽器渲染程式(核心):預設每個Tab頁面一個程式,互不影響,控制頁面渲染,指令碼執行,事件處理等(有時候會優化,如多個空白tab會合併成一個程式)
如下圖:
多執行緒的瀏覽器核心
每一個tab頁面可以看作是瀏覽器核心程式,然後這個程式是多執行緒的,它有幾大類子執行緒
-
GUI執行緒
-
JS引擎執行緒
-
事件觸發執行緒
-
定時器執行緒
-
網路請求執行緒
可以看到,裡面的JS引擎是核心程式中的一個執行緒,這也是為什麼常說JS引擎是單執行緒的
解析URL
輸入URL後,會進行解析(URL的本質就是統一資源定位符)
URL一般包括幾大部分:
-
protocol
,協議頭,譬如有http,ftp等 -
host
,主機域名或IP地址 -
port
,埠號 -
path
,目錄路徑 -
query
,即查詢引數 -
fragment
,即#
後的hash值,一般用來定位到某個位置
網路請求都是單獨的執行緒
每次網路請求時都需要開闢單獨的執行緒進行,譬如如果URL解析到http協議,就會新建一個網路執行緒去處理資源下載
因此瀏覽器會根據解析出得協議,開闢一個網路執行緒,前往請求資源(這裡,暫時理解為是瀏覽器核心開闢的,如有錯誤,後續修復)
更多
由於篇幅關係,這裡就大概介紹一個主幹流程,關於瀏覽器的程式機制,更多可以參考以前總結的一篇文章(因為內容實在過多,裡面包括JS執行機制,程式執行緒的詳解)
開啟網路執行緒到發出一個完整的http請求
這一部分主要內容包括:dns
查詢,tcp/ip
請求構建,五層因特網協議棧
等等
仍然是先梳理主幹,有些詳細的過程不展開(因為展開的話內容過多)
DNS查詢得到IP
如果輸入的是域名,需要進行dns解析成IP,大致流程:
-
如果瀏覽器有快取,直接使用瀏覽器快取,否則使用本機快取,再沒有的話就是用host
-
如果本地沒有,就向dns域名伺服器查詢(當然,中間可能還會經過路由,也有快取等),查詢到對應的IP
注意,域名查詢時有可能是經過了CDN排程器的(如果有cdn儲存功能的話)
而且,需要知道dns解析是很耗時的,因此如果解析域名過多,會讓首屏載入變得過慢,可以考慮dns-prefetch
優化
這一塊可以深入展開,具體請去網上搜尋,這裡就不佔篇幅了(網上可以看到很詳細的解答)
tcp/ip請求
http的本質就是tcp/ip
請求
需要了解3次握手規則建立連線以及斷開連線時的四次揮手
tcp將http長報文劃分為短報文,通過三次握手與服務端建立連線,進行可靠傳輸
三次握手的步驟:(抽象派)
客戶端:hello,你是server麼?
服務端:hello,我是server,你是client麼
客戶端:yes,我是client
複製程式碼
建立連線成功後,接下來就正式傳輸資料
然後,待到斷開連線時,需要進行四次揮手(因為是全雙工的,所以需要四次揮手)
四次揮手的步驟:(抽象派)
主動方:我已經關閉了向你那邊的主動通道了,只能被動接收了
被動方:收到通道關閉的資訊
被動方:那我也告訴你,我這邊向你的主動通道也關閉了
主動方:最後收到資料,之後雙方無法通訊
複製程式碼
tcp/ip的併發限制
瀏覽器對同一域名下併發的tcp連線是有限制的(2-10個不等)
而且在http1.0中往往一個資源下載就需要對應一個tcp/ip請求
所以針對這個瓶頸,又出現了很多的資源優化方案
get和post的區別
get和post雖然本質都是tcp/ip,但兩者除了在http層面外,在tcp/ip層面也有區別。
get會產生一個tcp資料包,post兩個
具體就是:
-
get請求時,瀏覽器會把
headers
和data
一起傳送出去,伺服器響應200(返回資料), -
post請求時,瀏覽器先傳送
headers
,伺服器響應100 continue
, 瀏覽器再傳送data
,伺服器響應200(返回資料)。
再說一點,這裡的區別是specification
(規範)層面,而不是implementation
(對規範的實現)
五層因特網協議棧
其實這個概念挺難記全的,記不全沒關係,但是要有一個整體概念
其實就是一個概念: 從客戶端發出http請求到伺服器接收,中間會經過一系列的流程。
簡括就是:
從應用層的傳送http請求,到傳輸層通過三次握手建立tcp/ip連線,再到網路層的ip定址,再到資料鏈路層的封裝成幀,最後到物理層的利用物理介質傳輸。
當然,服務端的接收就是反過來的步驟
五層因特網協議棧其實就是:
1.應用層(dns,http) DNS解析成IP併傳送http請求
2.傳輸層(tcp,udp) 建立tcp連線(三次握手)
3.網路層(IP,ARP) IP定址
4.資料鏈路層(PPP) 封裝成幀
5.物理層(利用物理介質傳輸位元流) 物理傳輸(然後傳輸的時候通過雙絞線,電磁波等各種介質)
複製程式碼
當然,其實也有一個完整的OSI七層框架,與之相比,多了會話層、表示層。
OSI七層框架:物理層
、資料鏈路層
、網路層
、傳輸層
、會話層
、表示層
、應用層
表示層:主要處理兩個通訊系統中交換資訊的表示方式,包括資料格式交換,資料加密與解密,資料壓縮與終端型別轉換等
會話層:它具體管理不同使用者和程式之間的對話,如控制登陸和登出過程
複製程式碼
從伺服器接收到請求到對應後臺接收到請求
服務端在接收到請求時,內部會進行很多的處理
這裡由於不是專業的後端分析,所以只是簡單的介紹下,不深入
負載均衡
對於大型的專案,由於併發訪問量很大,所以往往一臺伺服器是吃不消的,所以一般會有若干臺伺服器組成一個叢集,然後配合反向代理實現負載均衡
當然了,負載均衡不止這一種實現方式,這裡不深入...
簡單的說:
使用者發起的請求都指向排程伺服器(反向代理伺服器,譬如安裝了nginx控制負載均衡),然後排程伺服器根據實際的排程演算法,分配不同的請求給對應叢集中的伺服器執行,然後排程器等待實際伺服器的HTTP響應,並將它反饋給使用者
後臺的處理
一般後臺都是部署到容器中的,所以一般為:
-
先是容器接受到請求(如tomcat容器)
-
然後對應容器中的後臺程式接收到請求(如java程式)
-
然後就是後臺會有自己的統一處理,處理完後響應響應結果
概括下:
-
一般有的後端是有統一的驗證的,如安全攔截,跨域驗證
-
如果這一步不符合規則,就直接返回了相應的http報文(如拒絕請求等)
-
然後當驗證通過後,才會進入實際的後臺程式碼,此時是程式接收到請求,然後執行(譬如查詢資料庫,大量計算等等)
-
等程式執行完畢後,就會返回一個http響應包(一般這一步也會經過多層封裝)
-
然後就是將這個包從後端傳送到前端,完成互動
後臺和前臺的http互動
前後端互動時,http報文作為資訊的載體
所以http是一塊很重要的內容,這一部分重點介紹它
http報文結構
報文一般包括了:通用頭部
,請求/響應頭部
,請求/響應體
通用頭部
這也是開發人員見過的最多的資訊,包括如下:
Request Url: 請求的web伺服器地址
Request Method: 請求方式
(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 請求的返回狀態碼,如200代表成功
Remote Address: 請求的遠端伺服器地址(會轉為IP)
複製程式碼
譬如,在跨域拒絕時,可能是method為options
,狀態碼為404/405
等(當然,實際上可能的組合有很多)
其中,Method的話一般分為兩批次:
HTTP1.0定義了三種請求方法: GET, POST 和 HEAD方法。
以及幾種Additional Request Methods:PUT、DELETE、LINK、UNLINK
HTTP1.1定義了八種請求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
複製程式碼
HTTP 1.0
定義參考:tools.ietf.org/html/rfc194…
HTTP 1.1
定義參考:tools.ietf.org/html/rfc261…
這裡面最常用到的就是狀態碼,很多時候都是通過狀態碼來判斷,如(列舉幾個最常見的):
200——表明該請求被成功地完成,所請求的資源傳送回客戶端
304——自從上次請求後,請求的網頁未修改過,請客戶端使用本地快取
400——客戶端請求有錯(譬如可以是安全模組攔截)
401——請求未經授權
403——禁止訪問(譬如可以是未登入時禁止)
404——資源未找到
500——伺服器內部錯誤
503——服務不可用
...
複製程式碼
再列舉下大致不同範圍狀態的意義
1xx——指示資訊,表示請求已接收,繼續處理
2xx——成功,表示請求已被成功接收、理解、接受
3xx——重定向,要完成請求必須進行更進一步的操作
4xx——客戶端錯誤,請求有語法錯誤或請求無法實現
5xx——伺服器端錯誤,伺服器未能實現合法的請求
複製程式碼
總之,當請求出錯時,狀態碼能幫助快速定位問題,完整版本的狀態可以自行去網際網路搜尋
請求/響應頭部
請求和響應頭部也是分析時常用到的
常用的請求頭部(部分):
Accept: 接收型別,表示瀏覽器支援的MIME型別
(對標服務端返回的Content-Type)
Accept-Encoding:瀏覽器支援的壓縮型別,如gzip等,超出型別不能接收
Content-Type:客戶端傳送出去實體內容的型別
Cache-Control: 指定請求和響應遵循的快取機制,如no-cache
If-Modified-Since:對應服務端的Last-Modified,用來匹配看檔案是否變動,只能精確到1s之內,http1.0中
Expires:快取控制,在這個時間內不會請求,直接使用快取,http1.0,而且是服務端時間
Max-age:代表資源在本地快取多少秒,有效時間內不會請求,而是使用快取,http1.1中
If-None-Match:對應服務端的ETag,用來匹配檔案內容是否改變(非常精確),http1.1中
Cookie: 有cookie並且同域訪問時會自動帶上
Connection: 當瀏覽器與伺服器通訊時對於長連線如何進行處理,如keep-alive
Host:請求的伺服器URL
Origin:最初的請求是從哪裡發起的(只會精確到埠),Origin比Referer更尊重隱私
Referer:該頁面的來源URL(適用於所有型別的請求,會精確到詳細頁面地址,csrf攔截常用到這個欄位)
User-Agent:使用者客戶端的一些必要資訊,如UA頭部等
複製程式碼
常用的響應頭部(部分):
Access-Control-Allow-Headers: 伺服器端允許的請求Headers
Access-Control-Allow-Methods: 伺服器端允許的請求方法
Access-Control-Allow-Origin: 伺服器端允許的請求Origin頭部(譬如為*)
Content-Type:服務端返回的實體內容的型別
Date:資料從伺服器傳送的時間
Cache-Control:告訴瀏覽器或其他客戶,什麼環境可以安全的快取文件
Last-Modified:請求資源的最後修改時間
Expires:應該在什麼時候認為文件已經過期,從而不再快取它
Max-age:客戶端的本地資源應該快取多少秒,開啟了Cache-Control後有效
ETag:請求變數的實體標籤的當前值
Set-Cookie:設定和頁面關聯的cookie,伺服器通過這個頭部把cookie傳給客戶端
Keep-Alive:如果客戶端有keep-alive,服務端也會有響應(如timeout=38)
Server:伺服器的一些相關資訊
複製程式碼
一般來說,請求頭部和響應頭部是匹配分析的。
譬如,請求頭部的Accept
要和響應頭部的Content-Type
匹配,否則會報錯
譬如,跨域請求時,請求頭部的Origin
要匹配響應頭部的Access-Control-Allow-Origin
,否則會報跨域錯誤
譬如,在使用快取時,請求頭部的If-Modified-Since
、If-None-Match
分別和響應頭部的Last-Modified
、ETag
對應
還有很多的分析方法,這裡不一一贅述
請求/響應實體
http請求時,除了頭部,還有訊息實體,一般來說
請求實體中會將一些需要的引數都放入進入(用於post請求)。
譬如實體中可以放引數的序列化形式(a=1&b=2
這種),或者直接放表單物件(Form Data
物件,上傳時可以夾雜引數以及檔案),等等
而一般響應實體中,就是放服務端需要傳給客戶端的內容
一般現在的介面請求時,實體中就是對於的資訊的json格式,而像頁面請求這種,裡面就是直接放了一個html字串,然後瀏覽器自己解析並渲染。
CRLF
CRLF(Carriage-Return Line-Feed),意思是回車換行,一般作為分隔符存在
請求頭和實體訊息之間有一個CRLF分隔,響應頭部和響應實體之間用一個CRLF分隔
一般來說(分隔符類別):
CRLF->Windows-style
LF->Unix Style
CR->Mac Style
複製程式碼
如下圖是對某請求的http報文結構的簡要分析
cookie以及優化
cookie是瀏覽器的一種本地儲存方式,一般用來幫助客戶端和服務端通訊的,常用來進行身份校驗,結合服務端的session使用。
場景如下(簡述):
在登陸頁面,使用者登陸了
此時,服務端會生成一個session,session中有對於使用者的資訊(如使用者名稱、密碼等)
然後會有一個sessionid(相當於是服務端的這個session對應的key)
然後服務端在登入頁面中寫入cookie,值就是:jsessionid=xxx
然後瀏覽器本地就有這個cookie了,以後訪問同域名下的頁面時,自動帶上cookie,自動檢驗,在有效時間內無需二次登陸。
複製程式碼
上述就是cookie的常用場景簡述(當然了,實際情況下得考慮更多因素)
一般來說,cookie是不允許存放敏感資訊的(千萬不要明文儲存使用者名稱、密碼),因為非常不安全,如果一定要強行儲存,首先,一定要在cookie中設定httponly
(這樣就無法通過js操作了),另外可以考慮rsa等非對稱加密(因為實際上,瀏覽器本地也是容易被攻克的,並不安全)
另外,由於在同域名的資源請求時,瀏覽器會預設帶上本地的cookie,針對這種情況,在某些場景下是需要優化的。
譬如以下場景:
客戶端在域名A下有cookie(這個可以是登陸時由服務端寫入的)
然後在域名A下有一個頁面,頁面中有很多依賴的靜態資源(都是域名A的,譬如有20個靜態資源)
此時就有一個問題,頁面載入,請求這些靜態資源時,瀏覽器會預設帶上cookie
也就是說,這20個靜態資源的http請求,每一個都得帶上cookie,而實際上靜態資源並不需要cookie驗證
此時就造成了較為嚴重的浪費,而且也降低了訪問速度(因為內容更多了)
複製程式碼
當然了,針對這種場景,是有優化方案的(多域名拆分)。具體做法就是:
-
將靜態資源分組,分別放到不同的域名下(如
static.base.com
) -
而
page.base.com
(頁面所在域名)下請求時,是不會帶上static.base.com
域名的cookie的,所以就避免了浪費
說到了多域名拆分,這裡再提一個問題,那就是:
-
在移動端,如果請求的域名數過多,會降低請求速度(因為域名整套解析流程是很耗費時間的,而且移動端一般頻寬都比不上pc)
-
此時就需要用到一種優化方案:
dns-prefetch
(讓瀏覽器空閒時提前解析dns域名,不過也請合理使用,勿濫用)
關於cookie的互動,可以看下圖總結
gzip壓縮
首先,明確gzip
是一種壓縮格式,需要瀏覽器支援才有效(不過一般現在瀏覽器都支援),
而且gzip壓縮效率很好(高達70%左右)
然後gzip一般是由apache
、tomcat
等web伺服器開啟
當然伺服器除了gzip外,也還會有其它壓縮格式(如deflate,沒有gzip高效,且不流行)
所以一般只需要在伺服器上開啟了gzip壓縮,然後之後的請求就都是基於gzip壓縮格式的, 非常方便。
長連線與短連線
首先看tcp/ip
層面的定義:
-
長連線:一個tcp/ip連線上可以連續傳送多個資料包,在tcp連線保持期間,如果沒有資料包傳送,需要雙方發檢測包以維持此連線,一般需要自己做線上維持(類似於心跳包)
-
短連線:通訊雙方有資料互動時,就建立一個tcp連線,資料傳送完成後,則斷開此tcp連線
然後在http層面:
-
http1.0
中,預設使用的是短連線,也就是說,瀏覽器沒進行一次http操作,就建立一次連線,任務結束就中斷連線,譬如每一個靜態資源請求時都是一個單獨的連線 -
http1.1起,預設使用長連線,使用長連線會有這一行
Connection: keep-alive
,在長連線的情況下,當一個網頁開啟完成後,客戶端和服務端之間用於傳輸http的tcp連線不會關閉,如果客戶端再次訪問這個伺服器的頁面,會繼續使用這一條已經建立的連線
注意: keep-alive不會永遠保持,它有一個持續時間,一般在伺服器中配置(如apache),另外長連線需要客戶端和伺服器都支援時才有效
http 2.0
http2.0不是https,它相當於是http的下一代規範(譬如https的請求可以是http2.0規範的)
然後簡述下http2.0與http1.1的顯著不同點:
-
http1.1中,每請求一個資源,都是需要開啟一個tcp/ip連線的,所以對應的結果是,每一個資源對應一個tcp/ip請求,由於tcp/ip本身有併發數限制,所以當資源一多,速度就顯著慢下來
-
http2.0中,一個tcp/ip請求可以請求多個資源,也就是說,只要一次tcp/ip請求,就可以請求若干個資源,分割成更小的幀請求,速度明顯提升。
所以,如果http2.0全面應用,很多http1.1中的優化方案就無需用到了(譬如打包成精靈圖,靜態資源多域名拆分等)
然後簡述下http2.0的一些特性:
-
多路複用(即一個tcp/ip連線可以請求多個資源)
-
首部壓縮(http頭部壓縮,減少體積)
-
二進位制分幀(在應用層跟傳送層之間增加了一個二進位制分幀層,改進傳輸效能,實現低延遲和高吞吐量)
-
伺服器端推送(服務端可以對客戶端的一個請求發出多個響應,可以主動通知客戶端)
-
請求優先順序(如果流被賦予了優先順序,它就會基於這個優先順序來處理,由伺服器決定需要多少資源來處理該請求。)
https
https就是安全版本的http,譬如一些支付等操作基本都是基於https的,因為http請求的安全係數太低了。
簡單來看,https與http的區別就是: 在請求前,會建立ssl連結,確保接下來的通訊都是加密的,無法被輕易擷取分析
一般來說,如果要將網站升級成https,需要後端支援(後端需要申請證照等),然後https的開銷也比http要大(因為需要額外建立安全連結以及加密等),所以一般來說http2.0配合https的體驗更佳(因為http2.0更快了)
一般來說,主要關注的就是SSL/TLS的握手流程,如下(簡述):
1. 瀏覽器請求建立SSL連結,並向服務端傳送一個隨機數–Client random和客戶端支援的加密方法,比如RSA加密,此時是明文傳輸。
2. 服務端從中選出一組加密演算法與Hash演算法,回覆一個隨機數–Server random,並將自己的身份資訊以證照的形式發回給瀏覽器
(證照裡包含了網站地址,非對稱加密的公鑰,以及證照頒發機構等資訊)
3. 瀏覽器收到服務端的證照後
- 驗證證照的合法性(頒發機構是否合法,證照中包含的網址是否和正在訪問的一樣),如果證照信任,則瀏覽器會顯示一個小鎖頭,否則會有提示
- 使用者接收證照後(不管信不信任),瀏覽會生產新的隨機數–Premaster secret,然後證照中的公鑰以及指定的加密方法加密`Premaster secret`,傳送給伺服器。
- 利用Client random、Server random和Premaster secret通過一定的演算法生成HTTP連結資料傳輸的對稱加密key-`session key`
- 使用約定好的HASH演算法計算握手訊息,並使用生成的`session key`對訊息進行加密,最後將之前生成的所有資訊傳送給服務端。
4. 服務端收到瀏覽器的回覆
- 利用已知的加解密方式與自己的私鑰進行解密,獲取`Premaster secret`
- 和瀏覽器相同規則生成`session key`
- 使用`session key`解密瀏覽器發來的握手訊息,並驗證Hash是否與瀏覽器發來的一致
- 使用`session key`加密一段握手訊息,傳送給瀏覽器
5. 瀏覽器解密並計算握手訊息的HASH,如果與服務端發來的HASH一致,此時握手過程結束,
複製程式碼
之後所有的https通訊資料將由之前瀏覽器生成的session key
並利用對稱加密演算法進行加密
這裡放一張圖(來源:阮一峰-圖解SSL/TLS協議)
單獨拎出來的快取問題,http的快取
前後端的http互動中,使用快取能很大程度上的提升效率,而且基本上對效能有要求的前端專案都是必用快取的
強快取與弱快取
快取可以簡單的劃分成兩種型別:強快取
(200 from cache
)與協商快取
(304
)
區別簡述如下:
-
強快取(
200 from cache
)時,瀏覽器如果判斷本地快取未過期,就直接使用,無需發起http請求 -
協商快取(
304
)時,瀏覽器會向服務端發起http請求,然後服務端告訴瀏覽器檔案未改變,讓瀏覽器使用本地快取
對於協商快取,使用Ctrl + F5
強制重新整理可以使得快取無效
但是對於強快取,在未過期時,必須更新資源路徑才能發起新的請求(更改了路徑相當於是另一個資源了,這也是前端工程化中常用到的技巧)
快取頭部簡述
上述提到了強快取和協商快取,那它們是怎麼區分的呢?
答案是通過不同的http頭部控制
先看下這幾個頭部:
If-None-Match/E-tag、If-Modified-Since/Last-Modified、Cache-Control/Max-Age、Pragma/Expires
複製程式碼
這些就是快取中常用到的頭部,這裡不展開。僅列舉下大致使用。
屬於強快取控制的:
(http1.1)Cache-Control/Max-Age
(http1.0)Pragma/Expires
複製程式碼
注意:Max-Age
不是一個頭部,它是Cache-Control
頭部的值
屬於協商快取控制的:
(http1.1)If-None-Match/E-tag
(http1.0)If-Modified-Since/Last-Modified
複製程式碼
可以看到,上述有提到http1.1
和http1.0
,這些不同的頭部是屬於不同http時期的
再提一點,其實HTML頁面中也有一個meta標籤可以控制快取方案-Pragma
<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
複製程式碼
不過,這種方案還是比較少用到,因為支援情況不佳,譬如快取代理伺服器肯定不支援,所以不推薦
頭部的區別
首先明確,http的發展是從http1.0到http1.1
而在http1.1中,出了一些新內容,彌補了http1.0的不足。
http1.0中的快取控制:
-
Pragma
:嚴格來說,它不屬於專門的快取控制頭部,但是它設定no-cache
時可以讓本地強快取失效(屬於編譯控制,來實現特定的指令,主要是因為相容http1.0,所以以前又被大量應用) -
Expires
:服務端配置的,屬於強快取,用來控制在規定的時間之前,瀏覽器不會發出請求,而是直接使用本地快取,注意,Expires一般對應伺服器端時間,如Expires:Fri, 30 Oct 1998 14:19:41
-
If-Modified-Since/Last-Modified
:這兩個是成對出現的,屬於協商快取的內容,其中瀏覽器的頭部是If-Modified-Since
,而服務端的是Last-Modified
,它的作用是,在發起請求時,如果If-Modified-Since
和Last-Modified
匹配,那麼代表伺服器資源並未改變,因此服務端不會返回資源實體,而是隻返回頭部,通知瀏覽器可以使用本地快取。Last-Modified
,顧名思義,指的是檔案最後的修改時間,而且只能精確到1s
以內
http1.1中的快取控制:
-
Cache-Control
:快取控制頭部,有no-cache、max-age等多種取值 -
Max-Age
:服務端配置的,用來控制強快取,在規定的時間之內,瀏覽器無需發出請求,直接使用本地快取,注意,Max-Age是Cache-Control頭部的值,不是獨立的頭部,譬如Cache-Control: max-age=3600
,而且它值得是絕對時間,由瀏覽器自己計算 -
If-None-Match/E-tag
:這兩個是成對出現的,屬於協商快取的內容,其中瀏覽器的頭部是If-None-Match
,而服務端的是E-tag
,同樣,發出請求後,如果If-None-Match
和E-tag
匹配,則代表內容未變,通知瀏覽器使用本地快取,和Last-Modified不同,E-tag更精確,它是類似於指紋一樣的東西,基於FileEtag INode Mtime Size
生成,也就是說,只要檔案變,指紋就會變,而且沒有1s精確度的限制。
Max-Age相比Expires?
Expires
使用的是伺服器端的時間
但是有時候會有這樣一種情況-客戶端時間和服務端不同步
那這樣,可能就會出問題了,造成了瀏覽器本地的快取無用或者一直無法過期
所以一般http1.1後不推薦使用Expires
而Max-Age
使用的是客戶端本地時間的計算,因此不會有這個問題
因此推薦使用Max-Age
。
注意,如果同時啟用了Cache-Control
與Expires
,Cache-Control
優先順序高。
E-tag相比Last-Modified?
Last-Modified
:
-
表明服務端的檔案最後何時改變的
-
它有一個缺陷就是隻能精確到1s,
-
然後還有一個問題就是有的服務端的檔案會週期性的改變,導致快取失效
而E-tag
:
-
是一種指紋機制,代表檔案相關指紋
-
只有檔案變才會變,也只要檔案變就會變,
-
也沒有精確時間的限制,只要檔案一遍,立馬E-tag就不一樣了
如果同時帶有E-tag
和Last-Modified
,服務端會優先檢查E-tag
各大快取頭部的整體關係如下圖
解析頁面流程
前面有提到http互動,那麼接下來就是瀏覽器獲取到html,然後解析,渲染
這部分很多都參考了網上資源,特別是圖片,參考了來源中的文章
流程簡述
瀏覽器核心拿到內容後,渲染步驟大致可以分為以下幾步:
1. 解析HTML,構建DOM樹
2. 解析CSS,生成CSS規則樹
3. 合併DOM樹和CSS規則,生成render樹
4. 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算
5. 繪製render樹(paint),繪製頁面畫素資訊
6. 瀏覽器會將各層的資訊傳送給GPU,GPU會將各層合成(composite),顯示在螢幕上
複製程式碼
如下圖:
HTML解析,構建DOM
整個渲染步驟中,HTML解析是第一步。
簡單的理解,這一步的流程是這樣的:瀏覽器解析HTML,構建DOM樹。
但實際上,在分析整體構建時,卻不能一筆帶過,得稍微展開。
解析HTML到構建出DOM當然過程可以簡述如下:
Bytes → characters → tokens → nodes → DOM
複製程式碼
譬如假設有這樣一個HTML頁面:(以下部分的內容出自參考來源,修改了下格式)
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
</body>
</html>
複製程式碼
瀏覽器的處理如下:
列舉其中的一些重點過程:
1. Conversion轉換:瀏覽器將獲得的HTML內容(Bytes)基於他的編碼轉換為單個字元
2. Tokenizing分詞:瀏覽器按照HTML規範標準將這些字元轉換為不同的標記token。每個token都有自己獨特的含義以及規則集
3. Lexing詞法分析:分詞的結果是得到一堆的token,此時把他們轉換為物件,這些物件分別定義他們的屬性和規則
4. DOM構建:因為HTML標記定義的就是不同標籤之間的關係,這個關係就像是一個樹形結構一樣
例如:body物件的父節點就是HTML物件,然後段略p物件的父節點就是body物件
複製程式碼
最後的DOM樹如下:
生成CSS規則
同理,CSS規則樹的生成也是類似。簡述為:
Bytes → characters → tokens → nodes → CSSOM
複製程式碼
譬如style.css
內容如下:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
複製程式碼
那麼最終的CSSOM樹就是:
構建渲染樹
當DOM樹和CSSOM都有了後,就要開始構建渲染樹了
一般來說,渲染樹和DOM樹相對應的,但不是嚴格意義上的一一對應
因為有一些不可見的DOM元素不會插入到渲染樹中,如head這種不可見的標籤或者display: none
等
整體來說可以看圖:
渲染
有了render樹,接下來就是開始渲染,基本流程如下:
圖中重要的四個步驟就是:
1. 計算CSS樣式
2. 構建渲染樹
3. 佈局,主要定位座標和大小,是否換行,各種position overflow z-index屬性
4. 繪製,將影象繪製出來
複製程式碼
然後,圖中的線與箭頭代表通過js動態修改了DOM或CSS,導致了重新佈局(Layout)或渲染(Repaint)
這裡Layout和Repaint的概念是有區別的:
-
Layout,也稱為Reflow,即迴流。一般意味著元素的內容、結構、位置或尺寸發生了變化,需要重新計算樣式和渲染樹
-
Repaint,即重繪。意味著元素髮生的改變只是影響了元素的一些外觀之類的時候(例如,背景色,邊框顏色,文字顏色等),此時只需要應用新樣式繪製這個元素就可以了
迴流的成本開銷要高於重繪,而且一個節點的迴流往往回導致子節點以及同級節點的迴流, 所以優化方案中一般都包括,儘量避免迴流。
什麼會引起迴流?
1.頁面渲染初始化
2.DOM結構改變,比如刪除了某個節點
3.render樹變化,比如減少了padding
4.視窗resize
5.最複雜的一種:獲取某些屬性,引發迴流,
很多瀏覽器會對迴流做優化,會等到數量足夠時做一次批處理迴流,
但是除了render樹的直接變化,當獲取一些屬性時,瀏覽器為了獲得正確的值也會觸發迴流,這樣使得瀏覽器優化無效,包括
(1)offset(Top/Left/Width/Height)
(2) scroll(Top/Left/Width/Height)
(3) cilent(Top/Left/Width/Height)
(4) width,height
(5) 呼叫了getComputedStyle()或者IE的currentStyle
複製程式碼
迴流一定伴隨著重繪,重繪卻可以單獨出現
所以一般會有一些優化方案,如:
-
減少逐項更改樣式,最好一次性更改style,或者將樣式定義為class並一次性更新
-
避免迴圈操作dom,建立一個documentFragment或div,在它上面應用所有DOM操作,最後再把它新增到window.document
-
避免多次讀取offset等屬性。無法避免則將它們快取到變數
-
將複雜的元素絕對定位或固定定位,使得它脫離文件流,否則迴流代價會很高
注意:改變字型大小會引發迴流
再來看一個示例:
var s = document.body.style;
s.padding = "2px"; // 迴流+重繪
s.border = "1px solid red"; // 再一次 迴流+重繪
s.color = "blue"; // 再一次重繪
s.backgroundColor = "#ccc"; // 再一次 重繪
s.fontSize = "14px"; // 再一次 迴流+重繪
// 新增node,再一次 迴流+重繪
document.body.appendChild(document.createTextNode('abc!'));
複製程式碼
簡單層與複合層
上述中的渲染中止步於繪製,但實際上繪製這一步也沒有這麼簡單,它可以結合複合層和簡單層的概念來講。
這裡不展開,進簡單介紹下:
-
可以認為預設只有一個複合圖層,所有的DOM節點都是在這個複合圖層下的
-
如果開啟了硬體加速功能,可以將某個節點變成複合圖層
-
複合圖層之間的繪製互不干擾,由GPU直接控制
-
而簡單圖層中,就算是absolute等佈局,變化時不影響整體的迴流,但是由於在同一個圖層中,仍然是會影響繪製的,因此做動畫時效能仍然很低。而複合層是獨立的,所以一般做動畫推薦使用硬體加速
更多參考:
Chrome中的除錯
Chrome的開發者工具中,Performance中可以看到詳細的渲染過程:
資源外鏈的下載
上面介紹了html解析,渲染流程。但實際上,在解析html時,會遇到一些資源連線,此時就需要進行單獨處理了
簡單起見,這裡將遇到的靜態資源分為一下幾大類(未列舉所有):
-
CSS樣式資源
-
JS指令碼資源
-
img圖片類資源
遇到外鏈時的處理
當遇到上述的外鏈時,會單獨開啟一個下載執行緒去下載資源(http1.1中是每一個資源的下載都要開啟一個http請求,對應一個tcp/ip連結)
遇到CSS樣式資源
CSS資源的處理有幾個特點:
-
CSS下載時非同步,不會阻塞瀏覽器構建DOM樹
-
但是會阻塞渲染,也就是在構建render時,會等到css下載解析完畢後才進行(這點與瀏覽器優化有關,防止css規則不斷改變,避免了重複的構建)
-
有例外,
media query
宣告的CSS是不會阻塞渲染的
遇到JS指令碼資源
JS指令碼資源的處理有幾個特點:
-
阻塞瀏覽器的解析,也就是說發現一個外鏈指令碼時,需等待指令碼下載完成並執行後才會繼續解析HTML
-
瀏覽器的優化,一般現代瀏覽器有優化,在指令碼阻塞時,也會繼續下載其它資源(當然有併發上限),但是雖然指令碼可以並行下載,解析過程仍然是阻塞的,也就是說必須這個指令碼執行完畢後才會接下來的解析,並行下載只是一種優化而已
-
defer與async,普通的指令碼是會阻塞瀏覽器解析的,但是可以加上defer或async屬性,這樣指令碼就變成非同步了,可以等到解析完畢後再執行
注意,defer和async是有區別的: defer是延遲執行,而async是非同步執行。
簡單的說(不展開):
-
async
是非同步執行,非同步下載完畢後就會執行,不確保執行順序,一定在onload
前,但不確定在DOMContentLoaded
事件的前或後 -
defer
是延遲執行,在瀏覽器看起來的效果像是將指令碼放在了body
後面一樣(雖然按規範應該是在DOMContentLoaded
事件前,但實際上不同瀏覽器的優化效果不一樣,也有可能在它後面)
遇到img圖片類資源
遇到圖片等資源時,直接就是非同步下載,不會阻塞解析,下載完畢後直接用圖片替換原有src的地方
loaded和domcontentloaded
簡單的對比:
-
DOMContentLoaded 事件觸發時,僅當DOM載入完成,不包括樣式表,圖片(譬如如果有async載入的指令碼就不一定完成)
-
load 事件觸發時,頁面上所有的DOM,樣式表,指令碼,圖片都已經載入完成了
CSS的視覺化格式模型
這一部分內容很多參考《精通CSS-高階Web標準解決方案》以及參考來源
前面提到了整體的渲染概念,但實際上文件樹中的元素是按什麼渲染規則渲染的,是可以進一步展開的,此部分內容即: CSS的視覺化格式模型
先了解:
-
CSS中規定每一個元素都有自己的盒子模型(相當於規定了這個元素如何顯示)
-
然後視覺化格式模型則是把這些盒子按照規則擺放到頁面上,也就是如何佈局
-
換句話說,盒子模型規定了怎麼在頁面裡擺放盒子,盒子的相互作用等等
說到底: CSS的視覺化格式模型就是規定了瀏覽器在頁面中如何處理文件樹
關鍵字:
包含塊(Containing Block)
控制框(Controlling Box)
BFC(Block Formatting Context)
IFC(Inline Formatting Context)
定位體系
浮動
...
複製程式碼
另外,CSS有三種定位機制:普通流
,浮動
,絕對定位
,如無特別提及,下文中都是針對普通流中的
包含塊(Containing Block)
一個元素的box的定位和尺寸,會與某一矩形框有關,這個框就稱之為包含塊。
元素會為它的子孫元素建立包含塊,但是,並不是說元素的包含塊就是它的父元素,元素的包含塊與它的祖先元素的樣式等有關係
譬如:
-
根元素是最頂端的元素,它沒有父節點,它的包含塊就是初始包含塊
-
static和relative的包含塊由它最近的塊級、單元格或者行內塊祖先元素的內容框(content)建立
-
fixed的包含塊是當前可視視窗
-
absolute的包含塊由它最近的position 屬性為
absolute
、relative
或者fixed
的祖先元素建立-
如果其祖先元素是行內元素,則包含塊取決於其祖先元素的
direction
特性 -
如果祖先元素不是行內元素,那麼包含塊的區域應該是祖先元素的內邊距邊界
-
控制框(Controlling Box)
塊級元素和塊框以及行內元素和行框的相關概念
塊框:
-
塊級元素會生成一個塊框(
Block Box
),塊框會佔據一整行,用來包含子box和生成的內容 -
塊框同時也是一個塊包含框(
Containing Box
),裡面要麼只包含塊框,要麼只包含行內框(不能混雜),如果塊框內部有塊級元素也有行內元素,那麼行內元素會被匿名塊框包圍
關於匿名塊框的生成,示例:
<DIV>
Some text
<P>More text
</DIV>
複製程式碼
div
生成了一個塊框,包含了另一個塊框p
以及文字內容Some text
,此時Some text
文字會被強制加到一個匿名的塊框裡面,被div
生成的塊框包含(其實這個就是IFC
中提到的行框,包含這些行內框的這一行匿名塊形成的框,行框和行內框不同)
換句話說:
如果一個塊框在其中包含另外一個塊框,那麼我們強迫它只能包含塊框,因此其它文字內容生成出來的都是匿名塊框(而不是匿名行內框)
行內框:
-
一個行內元素生成一個行內框
-
行內元素能排在一行,允許左右有其它元素
關於匿名行內框的生成,示例:
<P>Some <EM>emphasized</EM> text</P>
複製程式碼
P
元素生成一個塊框,其中有幾個行內框(如EM
),以及文字Some
,text
,此時會專門為這些文字生成匿名行內框
display屬性的影響
display
的幾個屬性也可以影響不同框的生成:
-
block
,元素生成一個塊框 -
inline
,元素產生一個或多個的行內框 -
inline-block
,元素產生一個行內級塊框,行內塊框的內部會被當作塊塊來格式化,而此元素本身會被當作行內級框來格式化(這也是為什麼會產生BFC
) -
none
,不生成框,不再格式化結構中,當然了,另一個visibility: hidden
則會產生一個不可見的框
總結:
-
如果一個框裡,有一個塊級元素,那麼這個框裡的內容都會被當作塊框來進行格式化,因為只要出現了塊級元素,就會將裡面的內容分塊幾塊,每一塊獨佔一行(出現行內可以用匿名塊框解決)
-
如果一個框裡,沒有任何塊級元素,那麼這個框裡的內容會被當成行內框來格式化,因為裡面的內容是按照順序成行的排列
BFC(Block Formatting Context)
FC(格式上下文)?
FC即格式上下文,它定義框內部的元素渲染規則,比較抽象,譬如
FC像是一個大箱子,裡面裝有很多元素
箱子可以隔開裡面的元素和外面的元素(所以外部並不會影響FC內部的渲染)
內部的規則可以是:如何定位,寬高計算,margin摺疊等等
複製程式碼
不同型別的框參與的FC型別不同,譬如塊級框對應BFC,行內框對應IFC
注意,並不是說所有的框都會產生FC,而是符合特定條件才會產生,只有產生了對應的FC後才會應用對應渲染規則
BFC規則:
在塊格式化上下文中
每一個元素左外邊與包含塊的左邊相接觸(對於從右到左的格式化,右外邊接觸右邊)
即使存在浮動也是如此(所以浮動元素正常會直接貼近它的包含塊的左邊,與普通元素重合)
除非這個元素也建立了一個新的BFC
複製程式碼
總結幾點BFC特點:
-
內部
box
在垂直方向,一個接一個的放置 -
box的垂直方向由
margin
決定,屬於同一個BFC的兩個box間的margin會重疊 -
BFC區域不會與
float box
重疊(可用於排版) -
BFC就是頁面上的一個隔離的獨立容器,容器裡面的子元素不會影響到外面的元素。反之也如此
-
計算BFC的高度時,浮動元素也參與計算(不會浮動坍塌)
如何觸發BFC?
-
根元素
-
float
屬性不為none
-
position
為absolute
或fixed
-
display
為inline-block
,flex
,inline-flex
,table
,table-cell
,table-caption
-
overflow
不為visible
這裡提下,display: table
,它本身不產生BFC,但是它會產生匿名框(包含display: table-cell
的框),而這個匿名框產生BFC
更多請自行網上搜尋
IFC(Inline Formatting Context)
IFC即行內框產生的格式上下文
IFC規則
在行內格式化上下文中
框一個接一個地水平排列,起點是包含塊的頂部。
水平方向上的 margin,border 和 padding 在框之間得到保留
框在垂直方向上可以以不同的方式對齊:它們的頂部或底部對齊,或根據其中文字的基線對齊
複製程式碼
行框
包含那些框的長方形區域,會形成一行,叫做行框
行框的寬度由它的包含塊和其中的浮動元素決定,高度的確定由行高度計算規則決定
行框的規則:
如果幾個行內框在水平方向無法放入一個行框內,它們可以分配在兩個或多個垂直堆疊的行框中(即行內框的分割)
行框在堆疊時沒有垂直方向上的分割且永不重疊
行框的高度總是足夠容納所包含的所有框。不過,它可能高於它包含的最高的框(例如,框對齊會引起基線對齊)
行框的左邊接觸到其包含塊的左邊,右邊接觸到其包含塊的右邊。
複製程式碼
結合補充下IFC規則:
浮動元素可能會處於包含塊邊緣和行框邊緣之間
儘管在相同的行內格式化上下文中的行框通常擁有相同的寬度(包含塊的寬度),它們可能會因浮動元素縮短了可用寬度,而在寬度上發生變化
同一行內格式化上下文中的行框通常高度不一樣(如,一行包含了一個高的圖形,而其它行只包含文字)
當一行中行內框寬度的總和小於包含它們的行框的寬,它們在水平方向上的對齊,取決於 `text-align` 特性
空的行內框應該被忽略
即不包含文字,保留空白符,margin/padding/border非0的行內元素,
以及其他常規流中的內容(比如,圖片,inline blocks 和 inline tables),
並且不是以換行結束的行框,
必須被當作零高度行框對待
複製程式碼
總結:
-
行內元素總是會應用IFC渲染規則
-
行內元素會應用IFC規則渲染,譬如
text-align
可以用來居中等 -
塊框內部,對於文字這類的匿名元素,會產生匿名行框包圍,而行框內部就應用IFC渲染規則
-
行內框內部,對於那些行內元素,一樣應用IFC渲染規則
-
另外,
inline-block
,會在元素外層產生IFC(所以這個元素是可以通過text-align
水平居中的),當然,它內部則按照BFC規則渲染
相比BFC規則來說,IFC可能更加抽象(因為沒有那麼條理清晰的規則和觸發條件)
但總的來說,它就是行內元素自身如何顯示以及在框內如何擺放的渲染規則,這樣描述應該更容易理解
其它
當然還有有一些其它內容:
-
譬如常規流,浮動,絕對定位等區別
-
譬如浮動元素不包含在常規流中
-
譬如相對定位,絕對定位,
Fixed
定位等區別 -
譬如
z-index
的分層顯示機制等
這裡不一一展開,更多請參考:
JS引擎解析過程
前面有提到遇到JS指令碼時,會等到它的執行,實際上是需要引擎解析的,這裡展開描述(介紹主幹流程)
JS的解釋階段
首先得明確: JS是解釋型語音,所以它無需提前編譯,而是由直譯器實時執行
引擎對JS的處理過程可以簡述如下:
1. 讀取程式碼,進行詞法分析(Lexical analysis),然後將程式碼分解成詞元(token)
2. 對詞元進行語法分析(parsing),然後將程式碼整理成語法樹(syntax tree)
3. 使用翻譯器(translator),將程式碼轉為位元組碼(bytecode)
4. 使用位元組碼直譯器(bytecode interpreter),將位元組碼轉為機器碼
複製程式碼
最終計算機執行的就是機器碼。
為了提高執行速度,現代瀏覽器一般採用即時編譯(JIT-Just In Time compiler
)
即位元組碼只在執行時編譯,用到哪一行就編譯哪一行,並且把編譯結果快取(inline cache
)
這樣整個程式的執行速度能得到顯著提升。
而且,不同瀏覽器策略可能還不同,有的瀏覽器就省略了位元組碼的翻譯步驟,直接轉為機器碼(如chrome的v8)
總結起來可以認為是: 核心的JIT
編譯器將原始碼編譯成機器碼執行
JS的預處理階段
上述將的是直譯器的整體過程,這裡再提下在正式執行JS前,還會有一個預處理階段 (譬如變數提升,分號補全等)
預處理階段會做一些事情,確保JS可以正確執行,這裡僅提部分:
分號補全
JS執行是需要分號的,但為什麼以下語句卻可以正常執行呢?
console.log('a')
console.log('b')
複製程式碼
原因就是JS直譯器有一個Semicolon Insertion規則,它會按照一定規則,在適當的位置補充分號
譬如列舉幾條自動加分號的規則:
-
當有換行符(包括含有換行符的多行註釋),並且下一個
token
沒法跟前面的語法匹配時,會自動補分號。 -
當有
}
時,如果缺少分號,會補分號。 -
程式原始碼結束時,如果缺少分號,會補分號。
於是,上述的程式碼就變成了
console.log('a');
console.log('b');
複製程式碼
所以可以正常執行
當然了,這裡有一個經典的例子:
function b() {
return
{
a: 'a'
};
}
複製程式碼
由於分號補全機制,所以它變成了:
function b() {
return;
{
a: 'a'
};
}
複製程式碼
所以執行後是undefined
變數提升
一般包括函式提升和變數提升
譬如:
a = 1;
b();
function b() {
console.log('b');
}
var a;
複製程式碼
經過變數提升後,就變成:
function b() {
console.log('b');
}
var a;
a = 1;
b();
複製程式碼
這裡沒有展開,其實展開也可以牽涉到很多內容的
譬如可以提下變數宣告,函式宣告,形參,實參的優先順序順序,以及es6中let有關的臨時死區等
JS的執行階段
此階段的內容中的圖片來源:深入理解JavaScript系列(10):JavaScript核心(晉級高手必讀篇)
直譯器解釋完語法規則後,就開始執行,然後整個執行流程中大致包含以下概念:
-
執行上下文,執行堆疊概念(如全域性上下文,當前活動上下文)
-
VO(變數物件)和AO(活動物件)
-
作用域鏈
-
this機制等
這些概念如果深入講解的話內容過多,因此這裡僅提及部分特性
執行上下文簡單解釋
-
JS有
執行上下文
) -
瀏覽器首次載入指令碼,它將建立
全域性執行上下文
,並壓入執行棧棧頂(不可被彈出) -
然後每進入其它作用域就建立對應的執行上下文並把它壓入執行棧的頂部
-
一旦對應的上下文執行完畢,就從棧頂彈出,並將上下文控制權交給當前的棧。
-
這樣依次執行(最終都會回到全域性執行上下文)
譬如,如果程式執行完畢,被彈出執行棧,然後有沒有被引用(沒有形成閉包),那麼這個函式中用到的記憶體就會被垃圾處理器自動回收
然後執行上下文與VO,作用域鏈,this的關係是:
每一個執行上下文,都有三個重要屬性:
-
變數物件(
Variable object,VO
) -
作用域鏈(
Scope chain
) -
this
VO與AO
VO是執行上下文的屬性(抽象概念),但是只有全域性上下文的變數物件允許通過VO的屬性名稱來間接訪問(因為在全域性上下文裡,全域性物件自身就是變數物件)
AO(activation object
),當函式被呼叫者啟用,AO就被建立了
可以理解為:
-
在函式上下文中:
VO === AO
-
在全域性上下文中:
VO === this === global
總的來說,VO中會存放一些變數資訊(如宣告的變數,函式,arguments
引數等等)
作用域鏈
它是執行上下文中的一個屬性,原理和原型鏈很相似,作用很重要。
譬如流程簡述:
在函式上下文中,查詢一個變數foo
如果函式的VO中找到了,就直接使用
否則去它的父級作用域鏈中(__parent__)找
如果父級中沒找到,繼續往上找
直到全域性上下文中也沒找到就報錯
複製程式碼
this指標
這也是JS的核心知識之一,由於內容過多,這裡就不展開,僅提及部分
注意:this是執行上下文環境的一個屬性,而不是某個變數物件的屬性
因此:
-
this是沒有一個類似搜尋變數的過程
-
當程式碼中使用了this,這個 this的值就直接從執行的上下文中獲取了,而不會從作用域鏈中搜尋
-
this的值只取決中進入上下文時的情況
所以經典的例子:
var baz = 200;
var bar = {
baz: 100,
foo: function() {
console.log(this.baz);
}
};
var foo = bar.foo;
// 進入環境:global
foo(); // 200,嚴格模式中會報錯,Cannot read property 'baz' of undefined
// 進入環境:global bar
bar.foo(); // 100
複製程式碼
就要明白了上面this的介紹,上述例子很好理解
更多參考:
深入理解JavaScript系列(13):This? Yes,this!
回收機制
JS有垃圾處理器,所以無需手動回收記憶體,而是由垃圾處理器自動處理。
一般來說,垃圾處理器有自己的回收策略。
譬如對於那些執行完畢的函式,如果沒有外部引用(被引用的話會形成閉包),則會回收。(當然一般會把回收動作切割到不同的時間段執行,防止影響效能)
常用的兩種垃圾回收規則是:
-
標記清除
-
引用計數
Javascript引擎基礎GC方案是(simple GC
):mark and sweep
(標記清除),簡單解釋如下:
-
遍歷所有可訪問的物件。
-
回收已不可訪問的物件。
譬如:(出自javascript高程)
當變數進入環境時,例如,在函式中宣告一個變數,就將這個變數標記為“進入環境”。
從邏輯上講,永遠不能釋放進入環境的變數所佔用的記憶體,因為只要執行流進入相應的環境,就可能會用到它們。
而當變數離開環境時,則將其標記為“離開環境”。
垃圾回收器在執行的時候會給儲存在記憶體中的所有變數都加上標記(當然,可以使用任何標記方式)。
然後,它會去掉環境中的變數以及被環境中的變數引用的變數的標記(閉包,也就是說在環境中的以及相關引用的變數會被去除標記)。
而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。
最後,垃圾回收器完成記憶體清除工作,銷燬那些帶標記的值並回收它們所佔用的記憶體空間。
關於引用計數,簡單點理解:
跟蹤記錄每個值被引用的次數,當一個值被引用時,次數+1
,減持時-1
,下次垃圾回收器會回收次數為0
的值的記憶體(當然了,容易出迴圈引用的bug)
GC的缺陷
和其他語言一樣,javascript的GC策略也無法避免一個問題: GC時,停止響應其他操作
這是為了安全考慮。
而Javascript的GC在100ms
甚至以上
對一般的應用還好,但對於JS遊戲,動畫對連貫性要求比較高的應用,就麻煩了。
這就是引擎需要優化的點: 避免GC造成的長時間停止響應。
GC優化策略
這裡介紹常用到的:分代回收(Generation GC)
目的是通過區分“臨時”與“持久”物件:
-
多回收“臨時物件”區(
young generation
) -
少回收“持久物件”區(
tenured generation
) -
減少每次需遍歷的物件,從而減少每次GC的耗時。
像node v8引擎就是採用的分代回收(和java一樣,作者是java虛擬機器作者。)
更多可以參考:
其它
可以提到跨域
譬如發出網路請求時,會用AJAX,如果介面跨域,就會遇到跨域問題
可以參考:
可以提到web安全
譬如瀏覽器在解析HTML時,有XSSAuditor
,可以延伸到web安全相關領域
可以參考:
更多
如可以提到viewport
概念,講講物理畫素,邏輯畫素,CSS畫素等概念
如熟悉Hybrid開發的話可以提及一下Hybrid相關內容以及優化
...
總結
上述這麼多內容,目的是:梳理出自己的知識體系
本文由於是前端向,所以知識梳理時有重點,很多其它的知識點都簡述或略去了,重點介紹的模組總結:
-
瀏覽器的程式/執行緒模型、JS執行機制(這一塊的詳細介紹連結到了另一篇文章)
-
http規範(包括報文結構,頭部,優化,http2.0,https等)
-
http快取(單獨列出來,因為它很重要)
-
頁面解析流程(HTML解析,構建DOM,生成CSS規則,構建渲染樹,渲染流程,複合層的合成,外鏈的處理等)
-
JS引擎解析過程(包括解釋階段,預處理階段,執行階段,包括執行上下文、VO、作用域鏈、this、回收機制等)
-
跨域相關,web安全單獨連結到了具體文章,其它如CSS盒模型,viewport等僅是提及概念
關於本文的價值?
本文是個人階段性梳理知識體系的成果,然後加以修繕後釋出成文章,因此並不確保適用於所有人員
但是,個人認為本文還是有一定參考價值的
寫在最後的話
還是那句話:知識要形成體系
梳理出知識體系後,有了一個骨架,知識點不易遺忘,而且學習新知識時也會更加迅速,更重要的是容易舉一反三,可以由一個普通的問題,深挖擴充到底層原理
前端知識是無窮無盡的,本文也僅僅是簡單梳理出一個承載知識體系的骨架而已,更多的內容仍然需要不斷學習,積累
另外,本文結合從瀏覽器多程式到JS單執行緒,JS執行機制最全面的一次梳理這篇文章,更佳噢!
附錄
部落格
初次釋出2018.03.12
於我個人部落格上面
www.dailichun.com/2018/03/12/…