按照網上習慣性的文章撰寫路線,第二篇應該是 CSS,也确實,各種各樣的标簽,也是由CSS在背後默默地做屬性支援。但是對于 HTML 的很多問題,跟浏覽器的關系太大了,讓我不禁想去好好看看 HTML 跟浏覽器之間的那些事。另外,可能一提到前端,絕大多數人都會脫口而出“JavaScript”,不過我覺得 CSS 才是前端技術裡最精彩絕倫的技術,是以後面我會對 CSS 做一個特殊處理,先讓我先把浏覽器相關面試題做一份梳理吧。梳理的過程中,發現浏覽器跟 HTTP 息息相關,後面就放在同一篇啦,一起來享受吧。
先簡單認識一下什麼是浏覽器......
1、浏覽器的主要組成部分
- 使用者界面:除了浏覽器主視窗顯示的請求的頁面外,其他顯示的各個部分都屬于使用者界面。
- 浏覽器引擎:在使用者界面和呈現引擎之間傳送指令。
- 呈現引擎: 負責顯示請求的内容。如果請求的内容是 HTML,它就負責解析 HTML 和 CSS 内容,并将解析後的内容顯示在螢幕上。
- 網絡:用于網絡調用(如 HTTP 請求)。其接口與平台⽆關,并為所有平台提供底層實作。
- 使用者界⾯後端:用于繪制基本的視窗小部件,比如組合框和視窗。其公開了與平台⽆關的通用接口,而在底層使用作業系統的⽤戶界面方法。
- JavaScript 解釋器:用于解析和執行 JavaScript 代碼。
- 資料存儲:這是持久層。浏覽器需要在硬碟上儲存各種資料(如 Cookie)。HTML5 定義了“網絡資料庫”,這是⼀個完整(輕便)的浏覽器内資料庫。Chrome 浏覽器的每個标簽⻚都是⼀個獨立的程序。
2、浏覽器核心
什麼是浏覽器核心
浏覽器核心作為浏覽器最核心的部分 “Rendering Engine”,也就是渲染引擎,它決定了浏覽器如何解析網頁文法并渲染網頁内容。
常見的浏覽器核心
浏覽器 /RunTime | 核心(渲染引擎) | JavaScript 引擎 |
Chrome | Blink ( 28~ ) Webkit ( Chrome 27 ) | V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | Chakra(for JScript) |
PhantomJS | Webkit | JavaScriptCore |
Node.js | - | V8 |
上面其實做個簡單的了解就可以了,接下來是關于浏覽器我覺得最精彩的一個問題,其中又包含了多個經典的面試題。在以前的文章我有做過總結,這裡我再次梳理一下。
3、從浏覽器輸入一個url到顯示頁面經曆的過程
這個過程其實就是一次完整的 http(1.0) 請求過程:
- 浏覽器對輸入的網址進行DNS解析,得到對應的IP位址
- 根據這個IP,找到對應的伺服器,發起TCP連接配接(TCP的三次握手)
- 建立TCP連結後,發起HTTP請求
- 伺服器響應HTTP請求,浏覽器得到 html 代碼
- 浏覽器解析 html 代碼,再請求代碼中的資源(js、css、圖檔等)
- 浏覽器渲染頁面
- 伺服器斷開TCP連接配接(TCP的四次揮手)
整個過程清晰可見,讓我們開始逐個擊破吧 ~~~
URL是啥?
URL(Uniform Resource Locator),統一資源定位符,用于定位網際網路上資源,俗稱網址。
看一下它的定義規則:
scheme://host.domain:port/path/filename
各部分解釋如下:
- scheme:定義網際網路服務的類型。常見的協定有 http、https、ftp、file,其中最常見的類型是 http,而 https 則是進行加密的網絡傳輸。
- host:定義域主機(http 的預設主機是 www)
- domain:定義網際網路域名,比如 w3school.com.cn
- port:定義主機上的端口号(http 的預設端口号是 80)
- path:定義伺服器上的路徑(如果省略,則文檔必須位于網站的根目錄中)。
- filename:定義文檔/資源的名稱
DNS怎麼找到域名的?
我們先看幾個小概念
- DNS:一個網絡伺服器
- DNS 協定:提供通過域名查找 IP 位址,或逆向從 IP 位址反查域名的服務
- DNS域名解析:即在 DNS 上記錄一條資訊記錄(域名對應的 IP 位址)
那浏覽器如何通過域名去查詢 URL 對應的 IP 呢?
過程(面試):浏覽器自身域名緩存區找 =》作業系統的域名緩存區找 =》hosts檔案找 =》域名伺服器找
為什麼HTTP協定要基于TCP來實作?
TCP是一個端到端的可靠的面相連接配接的協定,HTTP基于傳輸層TCP協定,不用擔心資料傳輸的各種問題(當發生錯誤時,會重傳)
TCP的三次握手和四次揮手
三次握手:
- 第一次握手:建立連接配接時,用戶端發送syn包(syn=x)到伺服器,等待伺服器确認
- 第二次握手:伺服器收到syn包,必須确認客戶的SYN(ack=x+1),同時自己也發送一個SYN包(syn=y),即SYN+ACK包,
- 第三次握手:用戶端收到伺服器的SYN+ACK包,向伺服器發送确認包ACK(ack=y+1),此包發送完畢,用戶端和伺服器進入ESTABLISHED(TCP連接配接成功)狀态,完成三次握手
面試時可這樣講述:
- 第一次握手:由浏覽器發起,告訴伺服器我要發送請求了
- 第二次握手:由伺服器發起,告訴浏覽器我準備接受了,你趕緊發送吧
- 第三次握手:由浏覽器發送,告訴伺服器,我馬上就發了,準備接受吧
面試官可能還問你,為什麼需要三次握手?---- 為了防止已失效的連接配接請求封包段突然又傳送到了服務端,因而産生錯誤。
四次揮手:
- 第一次揮手:用戶端程序發出連接配接釋放封包(FIN封包),并且停止發送資料
- 第二次揮手:伺服器收到連接配接釋放封包,發出确認封包,ACK=1,ack=u+1,并且帶上自己的序列号seq=v
- 用戶端收到伺服器的确認請求後,此時,用戶端就進入FIN-WAIT-2(終止等待2)狀态,等待伺服器發送連接配接釋放封包(在這之前還需要接受伺服器發送的最後的資料)
- 第三次揮手:伺服器将最後的資料發送完畢後,就向用戶端發送連接配接釋放封包,FIN=1,ack=u+1
- 第四次揮手:用戶端收到伺服器的連接配接釋放封包後,必須發出确認,ACK=1,ack=w+1,而自己的序列号是seq=u+1。伺服器隻要收到了用戶端發出的确認,立即進入CLOSED狀态。
面試時可這樣講述:
- 第一次揮手:由浏覽器發起,告訴伺服器,我請求封包發送完了,你準備關閉吧
- 第二次揮手:由伺服器發起,告訴浏覽器,我請求封包接受完了,準備關閉了,你也準備吧
- 第三次揮手:由伺服器發起,告訴浏覽器,我響應封包發送完了,你準備關閉吧
- 第四次揮手:由浏覽器發起,告訴伺服器,我響應封包接受完了,準備關閉了,你也準備吧
浏覽器是如何渲染頁面的?
- 建構DOM樹:渲染引擎解析HTML文檔,将标簽轉換成DOM節點建構DOM樹
- 生成渲染樹:解析CSS檔案(生成CSS規則樹),再生成渲染樹,使每個節點有自己的樣式
- 布局渲染樹:從根節點遞歸調用,布局出每個節點的位置、尺寸(布局)
- 繪制渲染樹:周遊渲染樹,使用UI層繪制每個節點 ,呈現界面
既然涉及到了布局和渲染,那怎麼能少了重繪和回流呢 ~~~
4、對重繪和回流的了解
概念
- 重繪:元素外觀(如背景顔色)改變引起的浏覽器行為,使元素外觀重新繪制
- 回流(重排):渲染樹中的元素布局或幾何屬性(如尺寸、隐藏)等改變,需要重新建構
注意點
- 每個頁面至少發生一次回流,即頁面第一次加載的時候
- 回流必定引發重繪,重繪不一定引發回流
補充
觸發回流的條件:任何頁面布局或幾何屬性的改變
- 頁面渲染初始化(無法避免)
- 添加或删除可見的DOM元素
- 元素位置的改變,或使用動畫
- 元素尺寸的改變(大小,外邊距,邊框)
- 浏覽器視窗尺寸的變化(resize事件)
- 填充内容的改變(文本或圖檔大小改變,引起計算值寬高改變)
- 讀取某些元素屬性(offsetLeft/Top/Height/Width, clientLeft/Top/Height/Width,scrollLeft/Top/Height/Width等)
如何優化?
重繪回流會造成耗時、浏覽器卡頓,那麼如何做優化呢?(盡量減少DOM操作)
- 浏覽器優化:浏覽器會把引起回流、重繪的操作放入一個隊列,等隊列中的操作到了一定數量或者時間間隔,就 flush 這個隊列進行一個批處理。這樣就可以讓多次回流重繪變成一次。
- 代碼優化:減少對渲染樹的操作,可以合并多次 DOM 和樣式的修改,并減少對樣式的請求。
一些代碼優化操作舉例:
- 修改元素樣式的時候,直接修改樣式名className(盡量一次修改元素樣式,不要一會改一點。也就是把新樣式放在另一個樣式名中)
- 某些元素先設定成display: none,然後進行頁面布局操作,再設定display: block(這樣隻會引發兩次重繪回流)
- 使用cloneNode和repalceChild技術(引發一次重繪回流)
- 将需要多次回流的元素,position屬性設為absoluted或fixed(元素脫離文檔流,變化不會影響其他元素)
- 當需要建立多個節點的時候,使用DocumentFragment建立完後一次性的加入(如循環建立一個li,讓循環結束後所有的li都建立完了(fragment中)再一次性加入(文檔))
順利完成了一次資源的請求,我們浏覽器拿到了伺服器發送過來的資源,為了節省性能,當然少不了存儲了,那麼請列出你所知道的浏覽器的一些存儲辦法以及它們的差別吧 ~
5、cookie,session,storage的差別
這一塊以前做過一張還不錯的表,這裡附上,并做了一些改進。
本地存儲差別表
WebStorage(HTML5) | ||||
cookie | session(伺服器) | sessionstorage | localstorage | |
資料生命周期 | 一般由伺服器生成,在設定失效時間(expires)内有效(與視窗或浏覽器是否關閉無關)。若在浏覽器設定,預設浏覽器關閉後失效 | 除非被清除,否則永久儲存(重新整理頁面資料依舊存在) | 僅在目前會話有效,關閉視窗或浏覽器後清除 | 除非web應用主動删除,否則永不失效 |
存放資料 | 4kb左右,數量最多20條,存儲字元串 | 5M,存儲對象 | 5M,隻能存儲字元串 | |
與服務端通信 | 儲存在浏覽器端。每次都會攜帶在HTTP請求頭中,參與伺服器通信 | 儲存在伺服器端,不參與伺服器通信。會占用伺服器性能 | 儲存在用戶端(本地存儲),不參與伺服器通信 | |
安全性 | 安全性較低(cookie詐騙cookie截取) | session安全性大于cookie | ||
易用性 | 一般接口需要自己封裝 | 接口可以直接使用 | ||
作用域 | 在浏覽器所有的同源視窗中共享 | 不能在不同的浏覽器視窗共享 | 不能在不同的浏覽器視窗共享,在同源視窗中可以共享 | |
使用場景 | 主要用于儲存登入資訊 1、判斷使用者是否登入過網站,友善下次登入實作自動登入或記住密碼 2、上次登入的時間等資訊 3、上次檢視的頁面 4、浏覽計數 | 用于儲存每個使用者的專用資訊,變量的值儲存在伺服器端,通過sessionID來區分不同使用者。 1、購物車 2、使用者登入資訊 3、将某些資料放入session中,供同一使用者的不同頁面使用 4、防止使用者非法登入 | 敏感賬号一次性登入、表單,對于那種隻需要在使用者浏覽一組頁面期間儲存而關閉浏覽器後就可以丢棄的資料,sessionStorage會非常友善 | 常用于長期登入(判斷使用者是否登入),适合長期儲存在本地的資料。 購物車資訊、HTML5遊戲産生的一些本地資料 |
優點 | 具有極高的擴充性和可用性 | 1、存儲空間大 2、節省網絡流量 3、可在本地直接擷取,不需要與伺服器互動 4、擷取速度快 5、安全性較高 6、更多豐富易用的API接口 7、支援事件通知機制,可以将資料更新的通知發送給監聽者 8、操作友善:setItem、getItem、removeItem、clear、key、length 9、臨時存儲 | ||
缺點 | 1、大小受限 2、使用者可以禁用cookie,使功能受限 3、安全性較低 4、有些狀态不能儲存在用戶端 5、同源請求時會被攜帶(服務端和用戶端互傳,不論是否需要),加大http流量,資料過多影響性能 6、cookie資料有路徑(path)的概念,可以限制cookie隻屬于某個路徑下 |
對浏覽器的存儲方式有了清晰的了解以後,我們應該再思考,存下的資料如何使用會性能更好?存下的資料我們是否需要做适時的更新?浏覽器底層有什麼特别的機制?
6、浏覽器緩存政策
面試的時候,少不了關于性能優化的問題,基于上一個問題,不難想到浏覽器端應該做的一個優化手段 ==> 減少 HTTP 請求。為此我們可以做 HTTP 緩存控制,也就是浏覽器緩存政策。
關于浏覽器緩存的初步回答
- 浏覽器(HTTP)緩存能夠幫助伺服器提高并發性能,很多資源不需要重複請求,可直接從浏覽器中拿緩存(通過 HTTP 擷取的資源)
- 浏覽器緩存分類:強緩存、協商緩存
- 強緩存通過 Expires 和 Cache-control 控制,協商緩存通過 Last-modify 和 Etag 控制
這是一個比較籠統的回答,不過我覺得十分的精辟且可聊性較高。因為它引出了三個重要的問題:浏覽器的緩存政策、緩存分類及緩存通路。
浏覽器緩存政策(機制)
緩存政策主要發生在三個對象之間:浏覽器、浏覽器緩存、伺服器
- 浏覽器每次發起請求,都會先在浏覽器緩存中查找該請求的結果以及緩存辨別
- 浏覽器每次拿到傳回的請求結果都會将該結果和緩存辨別存入浏覽器緩存中
我們根據請求結果和緩存辨別來判斷是否需要向伺服器重新發起 HTTP 請求,而這個過程我們使用的緩存政策也不相同。
緩存政策都是通過設定 HTTP Header 來實作的,基于此我們把浏覽器緩存分為強緩存和協商緩存。這兩個緩存其實就是浏覽器做緩存時的不同處理過程。
強緩存
不會向伺服器發送請求,直接從浏覽器緩存中讀取資源
HTTP Header 實作: 和 Cache-Control
- Expires(http1.0):緩存過期時間(絕對時間),用來指定資源到期的時間,是伺服器端的具體的時間點(在響應http請求時告訴浏覽器在過期時間前浏覽器可以直接從浏覽器緩存取資料,而無需再次請求)。受限于本地時間,如果修改了本地時間,可能會造成緩存失效。
- Cache-Control(http1.1):是一個相對時間,代表資源的有效期。(優先)
現在基本上都會同時設定 Expires 和 Cache-Control
強緩存判斷是否緩存的依據來自于是否超出某個時間或者某個時間段,而不關心伺服器端檔案是否已經更新,這可能會導緻加載檔案不是伺服器端最新的内容,那我們如何獲知伺服器端内容是否已經發生了更新呢?此時我們需要用到協商緩存。
協商緩存
強緩存未命中,浏覽器攜帶緩存辨別向伺服器發起請求,由伺服器根據緩存辨別決定是否使用緩存的過程(判斷該辨別對應的資源是否更新)。
看看協商緩存的結果情況:
- 協商緩存生效,傳回 304 和 Not Modified,告訴浏覽器使用本地緩存
- 協商緩存失效,傳回 200 和 請求結果(新資源),并存入緩存
強緩存未命中,是什麼時候才未命中呢?協商緩存,協商的是什麼呢?
其實就是:浏覽器問伺服器,我緩存的資源有沒有更新啊?
- 沒有更新:浏覽器可以用緩存(304)
- 更新了:浏覽器不能用緩存,伺服器發新的給浏覽器(200)
HTTP Header 實作:Last-Modified 和 ETag (幫助浏覽器跟伺服器進行協商)
- Last-Modified:浏覽器第一次請求一個資源的時候,伺服器傳回的header中會加上Last-Modify,Last-modify是一個時間辨別該資源的最後修改時間 。當浏覽器再次請求該資源時,發送的請求頭中會包含If-Modify-Since,該值為緩存之前傳回的Last-Modify。伺服器收到If-Modify-Since後,根據資源的最後修改時間判斷是否命中緩存。如果命中緩存,則傳回http304,并且不會傳回資源内容,并且不會傳回Last-Modify。由于對比的服務端時間,是以用戶端與服務端時間差距不會導緻問題。但是有時候通過最後修改時間來判斷資源是否修改還是不太準确(Last-Modified 隻能以秒計時)。于是出現了ETag/If-None-Match,根據資源内容是否修改來決定緩存政策。
- Etag(http1.1):伺服器響應請求時,傳回目前資源檔案的一個唯一辨別(由伺服器生成),Etag/If-None-Match傳回的是一個校驗碼 。ETag可以保證每一個資源是唯一的,隻要資源有變化,Etag就會重新生成。伺服器根據浏覽器上發送的If-None-Match值來判斷是否命中緩存。
HTTP1.1中 Etag 的出現主要是為了解決幾個 Last-Modified 比較難解決的問題:
- Last-Modified标注的最後修改隻能精确到秒,如果某些檔案在1秒鐘以内,被修改多次的話,它将不能準确标注檔案的修改時間
- 如果某些檔案會被定期生成或者改完又改回來,内容并沒有任何變化,但Last-Modified卻改變了,導緻檔案沒法使用緩存
- 有可能存在伺服器沒有準确擷取檔案修改時間,或者與代理伺服器時間不一緻等情形
可能面試官會問你,Last-Modified 與 ETag 哪個更好呢?
- 精度上:Last-Modified 機關是秒,ETag 機關是每次, ETag精确度更優
- 性能上:Last-Modified 隻需要記錄時間,而 Etag 需要伺服器通過算法來計算出一個hash值。故 Last-Modified 性能更好
- 優先級:伺服器校驗優先考慮 Etag
關于優先級的補充:Etag 是伺服器自動生成或者由開發者生成的對應資源在伺服器端的唯一辨別符,能夠更加準确的控制緩存。Last-Modified 與 ETag 是可以一起使用的,伺服器會優先驗證 ETag,一緻的情況下,才會繼續比對 Last-Modified,最後才決定是否傳回304。
緩存通路
這個過程我們針對強緩存和協商緩存進行讨論(借用大佬的圖檔)
強緩存優先于協商緩存進行,若強緩存(Expires和Cache-Control)生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match),協商緩存由伺服器決定是否使用緩存,若協商緩存失效,那麼代表該請求的緩存失效,傳回200,重新傳回資源和緩存辨別,再存入浏覽器緩存中;生效則傳回304,繼續使用緩存。
唔 ~ 終于理完了,太不容易了。面試的時候能說到這裡已經很牛了哇,但是調皮的面試官可能還會問一個比較細的問題。 emmmm......
如果什麼緩存政策都沒設定,那麼浏覽器會怎麼處理?
對于這種情況,浏覽器會采用一個啟發式的算法,通常會取響應頭中的 Date 減去 Last-Modified 值的 10% 作為緩存時間。
其他
實際應用場景
- 頻繁變動的資源
- 不常變化的資源
使用者行為對浏覽器緩存的影響
新開視窗 | 有效 | 有效 |
---|---|---|
使用者操作 | Expires/Cache-Control | Last-Modified/Etag |
位址欄回車 | 有效 | 有效 |
頁面連結跳轉 | 有效 | 有效 |
前進、後退 | 有效 | 有效 |
F5 重新整理 | 無效 | 有效 |
Ctrl+F5 重新整理 | 無效 | 無效 |
說到浏覽器的政策,除了緩存政策,我們不得不提的就是浏覽器同源政策了 ~~
7、浏覽器同源政策
同源
源:
- 協定
- 域名
- 端口
同源即協定、域名和端口都相同。
什麼是同源政策
浏覽器的同源政策是一種安全功能,同源政策限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行互動。這是一個用于隔離潛在惡意檔案的安全機制(不同源之間,不能進行互動)
如 google.com下的 js腳本采用 ajax 讀取 baidu.com裡面的檔案資料是會報錯的。
既然說到 ajax ,腦海裡不禁湧現出一個又愛又恨的面試題了哈哈哈哈,敲了這麼多文字,動手來點代碼疏通下筋骨吧。
來,給我手寫一個原生 ajax ~~~ (噓,簡單的就行啦!)
var xhr = new XMLHttpRequest(); // 建立 Ajax 對象
xhr.open('get', 'https://blog.csdn.net/huohuoit'); // 告訴 Ajax 請求位址以及請求方式
xhr.send(); // 發送請求資料
xhr.onreadystatechange = function () { // 擷取伺服器端給與用戶端的響應資料
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
// 啊 ~~~ 報錯啦!~~~
限制問題
浏覽器中的大部分内容都是受同源政策限制的,如:
- Cookie、LocalStorage、IndexedDB 等存儲性内容
- DOM 節點
- AJAX 請求發送後,結果被浏覽器攔截了
- 一些第三方插件如 FLASH
但是有一些資源時不受同源政策限制的:
- 頁面中的連結,重定向以及表單送出
- <script>、<img>、<iframe>、<link>、<video> 等标簽
在浏覽器中,<script>、<img> 、<iframe>、<link>、<video> 等标簽都可以跨域加載資源,而不受同源政策的限制,通過 src 屬性加載的資源,浏覽器都會發起一個 GET 請求,但是浏覽器限制了 JavaScript 的權限,使用 js 不能讀、寫加載的内容。
你可以通過這幾個标簽來跨域加載資源,但是,發起的GET請求傳回的資料,通過 js 擷取不到。
注意:通過 <script> 标簽擷取 js 檔案裡的全局屬性,方法等,可以通過 js 讀取到。是因為這些都是挂載在 window對象上的。
上面我們提到了跨域,相信你經常會在面試題上看到這個問題,那就順着上文開始跟我一起拿下它吧。
8、跨域
什麼是跨域
什麼是跨域呢?我們上面提到了同源,同源中有“三個同”,其中一個就是“域”,但是這裡的域不是這個域哈,這裡的域是指“源”,也即域名位址。
上面我們讨論到,浏覽器同源政策下,會引起不同源之間不能進行互動的問題。那麼跨域其實就是解決不同源之間請求發送資料、通信等互動問題的解決方法。
跨域的實作
jsonp(最經典的跨域方案)
哦豁?json?jsonp?兩者其實沒啥關系,隻是 jsonp 請求後得到的是 json 資料格式
我們可以更詳細一點:
- jsonp 是 JSON With Padding(填充式 json 或參數式 json )的簡寫
- 組成(兩部分):回調函數 和 資料。回調函數是用來處理伺服器端傳回的資料,回調函數的名字一般是在請求中指定的。而資料就是我們需要擷取的資料,也就是伺服器端的資料。
jsonp 實作跨域的請求原理:
動态建立<script>标簽,然後利用<script>的 src 屬性不受同源政策限制來跨域擷取資料。
你可能會跟我一樣疑惑,為什麼是 <script> 标簽?
在上一個問題的最後,我們說到 <script>、<img>、<iframe>、<link>、<video>等标簽可以跨域加載資源,但是為啥隻有 <script>标簽可以請求到資料呢。
<script> 在請求得到資料後,遇到 js 代碼,就會解析執行( js 檔案裡寫的代碼肯定要被執行的)
jsonp 的 優點:
- 實作簡單
- 相容性⾮常好
jsonp 的缺點:
- 隻⽀持 get 請求(因為 <script> 标簽隻能get)
- 有安全性問題,容易遭受xss攻擊
- 需要服務端配合 jsonp 進⾏⼀定程度的改造
BulingBuling ~~~