11道瀏覽器原理面試題


瀏覽器與新技術

面試題來源於我的項目「前端面試與進階指南」

本章關於瀏覽器原理部分的內容主要來源於瀏覽器工作原理,這是一篇很長的文章,可以算上一本小書了,有精力的非常建議閱讀。

常見的瀏覽器內核有哪些?

瀏覽器/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

瀏覽器的主要組成部分是什么?

  1. 用戶界面 - 包括地址欄、前進/后退按鈕、書簽菜單等。除了瀏覽器主窗口顯示的您請求的頁面外,其他顯示的各個部分都屬於用戶界面。
  2. 瀏覽器引擎 - 在用戶界面和呈現引擎之間傳送指令。
  3. 呈現引擎 - 負責顯示請求的內容。如果請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析后的內容顯示在屏幕上。
  4. 網絡 - 用於網絡調用,比如 HTTP 請求。其接口與平台無關,並為所有平台提供底層實現。
  5. 用戶界面后端 - 用於繪制基本的窗口小部件,比如組合框和窗口。其公開了與平台無關的通用接口,而在底層使用操作系統的用戶界面方法。
  6. JavaScript 解釋器。用於解析和執行 JavaScript 代碼。
  7. 數據存儲。這是持久層。瀏覽器需要在硬盤上保存各種數據,例如 Cookie。新的 HTML 規范 (HTML5) 定義了“網絡數據庫”,這是一個完整(但是輕便)的瀏覽器內數據庫。

圖:瀏覽器的主要組件。

值得注意的是,和大多數瀏覽器不同,Chrome 瀏覽器的每個標簽頁都分別對應一個呈現引擎實例。每個標簽頁都是一個獨立的進程。

瀏覽器是如何渲染UI的?

  1. 瀏覽器獲取HTML文件,然后對文件進行解析,形成DOM Tree
  2. 與此同時,進行CSS解析,生成Style Rules
  3. 接着將DOM Tree與Style Rules合成為 Render Tree
  4. 接着進入布局(Layout)階段,也就是為每個節點分配一個應出現在屏幕上的確切坐標
  5. 隨后調用GPU進行繪制(Paint),遍歷Render Tree的節點,並將元素呈現出來

2019-06-22-06-48-02

瀏覽器如何解析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 如下

2019-06-22-06-58-56

若從左向右的匹配,過程是:

  1. 從 .mod-nav 開始,遍歷子節點 header 和子節點 div
  2. 然后各自向子節點遍歷。在右側 div 的分支中
  3. 最后遍歷到葉子節點 a ,發現不符合規則,需要回溯到 ul 節點,再遍歷下一個 li-a,一顆DOM樹的節點動不動上千,這種效率很低。

如果從右至左的匹配:

  1. 先找到所有的最右節點 span,對於每一個 span,向上尋找節點 h3
  2. 由 h3再向上尋找 class=mod-nav 的節點
  3. 最后找到根元素 html 則結束這個分支的遍歷。

后者匹配性能更好,是因為從右向左的匹配在第一步就篩選掉了大量的不符合條件的最右節點(葉子節點);而從左向右的匹配規則的性能都浪費在了失敗的查找上面。

DOM Tree是如何構建的?

  1. 轉碼: 瀏覽器將接收到的二進制數據按照指定編碼格式轉化為HTML字符串
  2. 生成Tokens: 之后開始parser,瀏覽器會將HTML字符串解析成Tokens
  3. 構建Nodes: 對Node添加特定的屬性,通過指針確定 Node 的父、子、兄弟關系和所屬 treeScope
  4. 生成DOM Tree: 通過node包含的指針確定的關系構建出DOM
    Tree

2019-06-22-11-48-00

瀏覽器重繪與重排的區別?

  • 重排: 部分渲染樹(或者整個渲染樹)需要重新分析並且節點尺寸需要重新計算,表現為重新生成布局,重新排列元素
  • 重繪: 由於節點的幾何屬性發生改變或者由於樣式發生改變,例如改變元素背景色時,屏幕上的部分內容需要更新,表現為某些元素的外觀被改變

單單改變元素的外觀,肯定不會引起網頁重新生成布局,但當瀏覽器完成重排之后,將會重新繪制受到此次重排影響的部分

重排和重繪代價是高昂的,它們會破壞用戶體驗,並且讓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高,不是嚴格的雙向通信,客戶端向服務端發送請求無法復用之前的連接,需要重新發出獨立的請求

2019-06-22-12-33-19

Websocket

使用指南請看WebSocket 教程

Websocket是一個全新的、獨立的協議,基於TCP協議,與http協議兼容、卻不會融入http協議,僅僅作為html5的一部分,其作用就是在服務器和客戶端之間建立實時的雙向通信。

  • 優點:真正意義上的實時雙向通信,性能好,低延遲
  • 缺點:獨立與http的協議,因此需要額外的項目改造,使用復雜度高,必須引入成熟的庫,無法兼容低版本瀏覽器

2019-06-22-12-33-43

Web Worker

后面性能優化部分會用到,先做了解

Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給后者運行

Web Worker教程

Service workers

后面性能優化部分會用到,先做了解

Service workers 本質上充當Web應用程序與瀏覽器之間的代理服務器,也可以在網絡可用時作為瀏覽器和網絡間的代理,創建有效的離線體驗。

Service workers教程

什么是瀏覽器同源策略?

同源策略限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。

同源是指"協議+域名+端口"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。

下表給出了相對http://store.company.com/dir/page.html同源檢測的示例:

2019-06-23-10-25-28

瀏覽器中的大部分內容都是受同源策略限制的,但是以下三個標簽可以不受限制:

  • <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的反向代理。

2019-06-24-10-19-34

反向代理的原理很簡單,即所有客戶端的請求都必須先經過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;
        }

    }

}

其它跨域方案

  1. HTML5 XMLHttpRequest 有一個API,postMessage()方法允許來自不同源的腳本采用異步方式進行有限的通信,可以實現跨文本檔、多窗口、跨域消息傳遞。
  2. WebSocket 是一種雙向通信協議,在建立連接之后,WebSocket 的 server 與 client 都能主動向對方發送或接收數據,連接建立好了之后 client 與 server 之間的雙向通信就與 HTTP 無關了,因此可以跨域。
  3. window.name + iframe:window.name屬性值在不同的頁面(甚至不同域名)加載后依舊存在,並且可以支持非常長的 name 值,我們可以利用這個特點進行跨域。
  4. location.hash + iframe:a.html欲與c.html跨域相互通信,通過中間頁b.html來實現。 三個頁面,不同域之間利用iframe的location.hash傳值,相同域之間直接js訪問來通信。
  5. document.domain + iframe: 該方式只能用於二級域名相同的情況下,比如 a.test.com 和 b.test.com 適用於該方式,我們只需要給頁面添加 document.domain ='test.com' 表示二級域名都相同就可以實現跨域,兩個頁面都通過js強制設置document.domain為基礎主域,就實現了同域。

其余方案來源於九種跨域方式


參考文章:


公眾號

想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號程序員面試官,后續的文章會優先在公眾號更新.

簡歷模板: 關注公眾號回復「模板」獲取

《前端面試手冊》: 配套於本指南的突擊手冊,關注公眾號回復「fed」獲取

2019-08-12-03-18-41


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM