瀏覽器與新技術 面試題來源於我的項目 "「前端面試與進階指南」" 本章關於瀏覽器原理部分的內容主要來源於 "瀏覽器工作原理" ,這是一篇很長的文章,可以算上一本小書了,有精力的非常建議閱讀。 常見的瀏覽器內核有哪些? | 瀏覽器/RunTime | 內核(渲染引擎) | JavaScript 引擎 ...
瀏覽器與新技術
面試題來源於我的項目「前端面試與進階指南」
本章關於瀏覽器原理部分的內容主要來源於瀏覽器工作原理,這是一篇很長的文章,可以算上一本小書了,有精力的非常建議閱讀。
常見的瀏覽器內核有哪些?
瀏覽器/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 |
瀏覽器的主要組成部分是什麼?
- 用戶界面 - 包括地址欄、前進/後退按鈕、書簽菜單等。除了瀏覽器主視窗顯示的您請求的頁面外,其他顯示的各個部分都屬於用戶界面。
- 瀏覽器引擎 - 在用戶界面和呈現引擎之間傳送指令。
- 呈現引擎 - 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。
- 網路 - 用於網路調用,比如 HTTP 請求。其介面與平臺無關,併為所有平臺提供底層實現。
- 用戶界面後端 - 用於繪製基本的視窗小部件,比如組合框和視窗。其公開了與平臺無關的通用介面,而在底層使用操作系統的用戶界面方法。
- JavaScript 解釋器。用於解析和執行 JavaScript 代碼。
- 數據存儲。這是持久層。瀏覽器需要在硬碟上保存各種數據,例如 Cookie。新的 HTML 規範 (HTML5) 定義了“網路資料庫”,這是一個完整(但是輕便)的瀏覽器內資料庫。
圖:瀏覽器的主要組件。
值得註意的是,和大多數瀏覽器不同,Chrome 瀏覽器的每個標簽頁都分別對應一個呈現引擎實例。每個標簽頁都是一個獨立的進程。
瀏覽器是如何渲染UI的?
- 瀏覽器獲取HTML文件,然後對文件進行解析,形成DOM Tree
- 與此同時,進行CSS解析,生成Style Rules
- 接著將DOM Tree與Style Rules合成為 Render Tree
- 接著進入佈局(Layout)階段,也就是為每個節點分配一個應出現在屏幕上的確切坐標
- 隨後調用GPU進行繪製(Paint),遍歷Render Tree的節點,並將元素呈現出來
瀏覽器如何解析css選擇器?
瀏覽器會『從右往左』解析CSS選擇器。
我們知道DOM Tree與Style Rules合成為 Render Tree,實際上是需要將Style Rules附著到DOM Tree上,因此需要根據選擇器提供的信息對DOM Tree進行遍歷,才能將樣式附著到對應的DOM元素上。
以下這段css為例
.mod-nav h3 span {font-size: 16px;}
我們對應的DOM Tree 如下
若從左向右的匹配,過程是:
- 從 .mod-nav 開始,遍歷子節點 header 和子節點 div
- 然後各自向子節點遍歷。在右側 div 的分支中
- 最後遍歷到葉子節點 a ,發現不符合規則,需要回溯到 ul 節點,再遍歷下一個 li-a,一顆DOM樹的節點動不動上千,這種效率很低。
如果從右至左的匹配:
- 先找到所有的最右節點 span,對於每一個 span,向上尋找節點 h3
- 由 h3再向上尋找 class=mod-nav 的節點
- 最後找到根元素 html 則結束這個分支的遍歷。
後者匹配性能更好,是因為從右向左的匹配在第一步就篩選掉了大量的不符合條件的最右節點(葉子節點);而從左向右的匹配規則的性能都浪費在了失敗的查找上面。
DOM Tree是如何構建的?
- 轉碼: 瀏覽器將接收到的二進位數據按照指定編碼格式轉化為HTML字元串
- 生成Tokens: 之後開始parser,瀏覽器會將HTML字元串解析成Tokens
- 構建Nodes: 對Node添加特定的屬性,通過指針確定 Node 的父、子、兄弟關係和所屬 treeScope
- 生成DOM Tree: 通過node包含的指針確定的關係構建出DOM
Tree
瀏覽器重繪與重排的區別?
- 重排: 部分渲染樹(或者整個渲染樹)需要重新分析並且節點尺寸需要重新計算,表現為重新生成佈局,重新排列元素
- 重繪: 由於節點的幾何屬性發生改變或者由於樣式發生改變,例如改變元素背景色時,屏幕上的部分內容需要更新,表現為某些元素的外觀被改變
單單改變元素的外觀,肯定不會引起網頁重新生成佈局,但當瀏覽器完成重排之後,將會重新繪製受到此次重排影響的部分
重排和重繪代價是高昂的,它們會破壞用戶體驗,並且讓UI展示非常遲緩,而相比之下重排的性能影響更大,在兩者無法避免的情況下,一般我們寧可選擇代價更小的重繪。
『重繪』不一定會出現『重排』,『重排』必然會出現『重繪』。
如何觸發重排和重繪?
任何改變用來構建渲染樹的信息都會導致一次重排或重繪:
- 添加、刪除、更新DOM節點
- 通過display: none隱藏一個DOM節點-觸發重排和重繪
- 通過visibility: hidden隱藏一個DOM節點-只觸發重繪,因為沒有幾何變化
- 移動或者給頁面中的DOM節點添加動畫
- 添加一個樣式表,調整樣式屬性
- 用戶行為,例如調整視窗大小,改變字型大小,或者滾動。
如何避免重繪或者重排?
集中改變樣式
我們往往通過改變class的方式來集中改變樣式
// 判斷是否是黑色系樣式
const theme = isDark ? 'dark' : 'light'
// 根據判斷來設置不同的class
ele.setAttribute('className', theme)
使用DocumentFragment
我們可以通過createDocumentFragment創建一個游離於DOM樹之外的節點,然後在此節點上批量操作,最後插入DOM樹中,因此只觸發一次重排
var fragment = document.createDocumentFragment();
for (let i = 0;i<10;i++){
let node = document.createElement("p");
node.innerHTML = i;
fragment.appendChild(node);
}
document.body.appendChild(fragment);
提升為合成層
將元素提升為合成層有以下優點:
- 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
- 對於 transform 和 opacity 效果,不會觸發 layout 和 paint
提升合成層的最好方式是使用 CSS 的 will-change 屬性:
#target {
will-change: transform;
}
關於合成層的詳解請移步無線性能優化:Composite
前端如何實現即時通訊?
短輪詢
短輪詢的原理很簡單,每隔一段時間客戶端就發出一個請求,去獲取伺服器最新的數據,一定程度上模擬實現了即時通訊。
- 優點:相容性強,實現非常簡單
- 缺點:延遲性高,非常消耗請求資源,影響性能
comet
comet有兩種主要實現手段,一種是基於 AJAX 的長輪詢(long-polling)方式,另一種是基於 Iframe 及 htmlfile 的流(streaming)方式,通常被叫做長連接。
具體兩種手段的操作方法請移步Comet技術詳解:基於HTTP長連接的Web端實時通信技術
長輪詢優缺點:
- 優點:相容性好,資源浪費較小
- 缺點:伺服器hold連接會消耗資源,返回數據順序無保證,難於管理維護
長連接優缺點:
- 優點:相容性好,消息即時到達,不發無用請求
- 缺點:伺服器維護長連接消耗資源
SSE
使用指南請看Server-Sent Events 教程
SSE(Server-Sent Event,服務端推送事件)是一種允許服務端向客戶端推送新數據的HTML5技術。
- 優點:基於HTTP而生,因此不需要太多改造就能使用,使用方便,而websocket非常複雜,必須藉助成熟的庫或框架
- 缺點:基於文本傳輸效率沒有websocket高,不是嚴格的雙向通信,客戶端向服務端發送請求無法復用之前的連接,需要重新發出獨立的請求
Websocket
使用指南請看WebSocket 教程
Websocket是一個全新的、獨立的協議,基於TCP協議,與http協議相容、卻不會融入http協議,僅僅作為html5的一部分,其作用就是在伺服器和客戶端之間建立實時的雙向通信。
- 優點:真正意義上的實時雙向通信,性能好,低延遲
- 缺點:獨立與http的協議,因此需要額外的項目改造,使用複雜度高,必須引入成熟的庫,無法相容低版本瀏覽器
Web Worker
後面性能優化部分會用到,先做瞭解
Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給後者運行
Service workers
後面性能優化部分會用到,先做瞭解
Service workers 本質上充當Web應用程式與瀏覽器之間的代理伺服器,也可以在網路可用時作為瀏覽器和網路間的代理,創建有效的離線體驗。
什麼是瀏覽器同源策略?
同源策略限制了從同一個源載入的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。
同源是指"協議+功能變數名稱+埠"三者相同,即便兩個不同的功能變數名稱指向同一個ip地址,也非同源。
下表給出了相對http://store.company.com/dir/page.html同源檢測的示例:
瀏覽器中的大部分內容都是受同源策略限制的,但是以下三個標簽可以不受限制:
<img src=XXX>
<link href=XXX>
<script src=XXX>
如何實現跨域?
跨域是個比較古老的命題了,歷史上跨域的實現手段有很多,我們現在主要介紹三種比較主流的跨域方案,其餘的方案我們就不深入討論了,因為使用場景很少,也沒必要記這麼多奇技淫巧。
最經典的跨域方案jsonp
jsonp本質上是一個Hack,它利用<script>
標簽不受同源策略限制的特性進行跨域操作。
jsonp優點:
- 實現簡單
- 相容性非常好
jsonp的缺點:
- 只支持get請求(因為
<script>
標簽只能get) - 有安全性問題,容易遭受xss攻擊
- 需要服務端配合jsonp進行一定程度的改造
jsonp的實現:
function JSONP({
url,
params,
callbackKey,
callback
}) {
// 在參數里制定 callback 的名字
params = params || {}
params[callbackKey] = 'jsonpCallback'
// 預留 callback
window.jsonpCallback = callback
// 拼接參數字元串
const paramKeys = Object.keys(params)
const paramString = paramKeys
.map(key => `${key}=${params[key]}`)
.join('&')
// 插入 DOM 元素
const script = document.createElement('script')
script.setAttribute('src', `${url}?${paramString}`)
document.body.appendChild(script)
}
JSONP({
url: 'http://s.weibo.com/ajax/jsonp/suggestion',
params: {
key: 'test',
},
callbackKey: '_cb',
callback(result) {
console.log(result.data)
}
})
最流行的跨域方案cors
cors是目前主流的跨域解決方案,跨域資源共用(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不同源伺服器上的指定的資源。當一個資源從與該資源本身所在的伺服器不同的域、協議或埠請求一個資源時,資源會發起一個跨域 HTTP 請求。
如果你用express,可以這樣在後端設置
//CORS middleware
var allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://example.com');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
}
//...
app.configure(function() {
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({ secret: 'cool beans' }));
app.use(express.methodOverride());
app.use(allowCrossDomain);
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
在生產環境中建議用成熟的開源中間件解決問題。
最方便的跨域方案Nginx
nginx是一款極其強大的web伺服器,其優點就是輕量級、啟動快、高併發。
現在的新項目中nginx幾乎是首選,我們用node或者java開發的服務通常都需要經過nginx的反向代理。
反向代理的原理很簡單,即所有客戶端的請求都必須先經過nginx的處理,nginx作為代理伺服器再講請求轉發給node或者java服務,這樣就規避了同源策略。
#進程, 可更具cpu數量調整
worker_processes 1;
events {
#連接數
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
#連接超時時間,伺服器會在這個時間過後關閉連接。
keepalive_timeout 10;
# gizp壓縮
gzip on;
# 直接請求nginx也是會報跨域錯誤的這裡設置允許跨域
# 如果代理地址已經允許跨域則不需要這些, 否則報錯(雖然這樣nginx跨域就沒意義了)
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
# srever模塊配置是http模塊中的一個子模塊,用來定義一個虛擬訪問主機
server {
listen 80;
server_name localhost;
# 根路徑指到index.html
location / {
root html;
index index.html index.htm;
}
# localhost/api 的請求會被轉發到192.168.0.103:8080
location /api {
rewrite ^/b/(.*)$ /$1 break; # 去除本地介面/api首碼, 否則會出現404
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.0.103:8080; # 轉發地址
}
# 重定向錯誤頁面到/50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
其它跨域方案
- HTML5 XMLHttpRequest 有一個API,postMessage()方法允許來自不同源的腳本採用非同步方式進行有限的通信,可以實現跨文本檔、多視窗、跨域消息傳遞。
- WebSocket 是一種雙向通信協議,在建立連接之後,WebSocket 的 server 與 client 都能主動向對方發送或接收數據,連接建立好了之後 client 與 server 之間的雙向通信就與 HTTP 無關了,因此可以跨域。
- window.name + iframe:window.name屬性值在不同的頁面(甚至不同功能變數名稱)載入後依舊存在,並且可以支持非常長的 name 值,我們可以利用這個特點進行跨域。
- location.hash + iframe:a.html欲與c.html跨域相互通信,通過中間頁b.html來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通信。
- document.domain + iframe: 該方式只能用於二級功能變數名稱相同的情況下,比如 a.test.com 和 b.test.com 適用於該方式,我們只需要給頁面添加 document.domain ='test.com' 表示二級功能變數名稱都相同就可以實現跨域,兩個頁面都通過js強制設置document.domain為基礎主域,就實現了同域。
其餘方案來源於九種跨域方式
參考文章:
公眾號
想要實時關註筆者最新的文章和最新的文檔更新請關註公眾號程式員面試官,後續的文章會優先在公眾號更新.
簡歷模板: 關註公眾號回覆「模板」獲取
《前端面試手冊》: 配套於本指南的突擊手冊,關註公眾號回覆「fed」獲取