瀏覽器相關原理(面試題)詳細總結二

1. 瀏覽器渲染過程是怎樣的?

按照渲染的時間順序,流水線可分爲以下幾個子階段:構建 DOM 樹樣式計算佈局階段分層柵格化顯示css

  1. 渲染進程將 HTML 內容轉換爲可以讀懂DOM 樹結構。
  2. 渲染引擎將 CSS 樣式錶轉化爲瀏覽器能夠理解的styleSheets,計算出 DOM 節點的樣式。
  3. 建立佈局樹,並計算元素的佈局信息。
  4. 對佈局樹進行分層,並生成分層樹。
  5. 爲每一個圖層生成繪製列表,並將其提交到合成線程。合成線程將圖層分圖塊,並柵格化將圖塊轉換成位圖。
  6. 合成線程發送繪製圖塊命令給瀏覽器進程。瀏覽器進程根據指令生成頁面,並顯示到顯示器上。

瀏覽器從網絡或硬盤中得到HTML字節數據後會通過一個流程將字節解析爲DOM樹,先將HTML的原始字節數據轉換爲文件指定編碼的字符,而後瀏覽器會根據HTML規範來將字符串轉換成各類令牌標籤,如html、body等。最終解析成一個樹狀的對象模型,就是dom樹;html

獲取css,獲取style標籤內的css、或者內嵌的css,或者當HTML代碼碰見標籤時,瀏覽器會發送請求得到該標籤中標記的CSS,當渲染引擎接收到 CSS 文本時,會執行一個轉換操做,將 CSS 文本轉換爲瀏覽器能夠理解的styleSheets前端

建立佈局樹,遍歷 DOM 樹中的全部可見節點,並把這些節點加到佈局中;而不可見的節點會被佈局樹忽略掉,如 head 標籤下面的所有內容,再好比 body.p.span 這個元素,由於它的屬性包含 dispaly:none,因此這個元素也沒有被包進佈局樹。最後計算 DOM 元素的佈局信息,使其都保存在佈局樹中。佈局完成過程當中,若是有js操做或者其餘操做,對元素的顏色,背景等做出改變就會引發重繪,若是有對元素的大小、定位等有改變則會引發迴流。git

由於頁面中有不少複雜的效果,如一些複雜的 3D 變換、頁面滾動,或者使用 z-indexing 作 z 軸排序等,爲了更加方便地實現這些效果,渲染引擎還須要爲特定的節點生成專用的圖層,並生成一棵對應的圖層樹。github

渲染引擎實現圖層的繪製,把一個圖層的繪製拆分紅不少小的繪製指令而後再把這些指令按照順序組成一個待繪製列表,當圖層的繪製列表準備好以後,主線程會把該繪製列表提交給合成線程,合成線程會將圖層劃分爲圖塊,而後按照視口附近的圖塊來優先生成位圖(實際生成位圖的操做是由柵格化來執行的。所謂柵格化,是指將圖塊轉換爲位圖)web

一旦全部圖塊都被光柵化,合成線程就會生成一個繪製圖塊的命令,而後將該命令提交給瀏覽器進程,瀏覽器最後進行顯示。瀏覽器

2.如何理解迴流和重繪?

迴流:當咱們對 DOM 的修改引起了 DOM 幾何尺寸的變化(好比修改元素的寬、高或隱藏元素等)時,瀏覽器須要從新計算元素的幾何屬性(其餘元素的幾何屬性和位置也會所以受到影響),而後再將計算的結果繪製出來。這個過程就是迴流(也叫重排)。緩存

重繪:當咱們對 DOM 的修改致使了樣式的變化、卻並未影響其幾何屬性(好比修改了顏色或背景色)時,瀏覽器不需從新計算元素的幾何屬性、直接爲該元素繪製新的樣式(跳過了上圖所示的迴流環節)。這個過程叫作重繪。 由此咱們能夠看出,重繪不必定致使迴流,迴流必定會致使重繪。微信

常見的會致使迴流的元素:
  • 常見的幾何屬性有 width、height、padding、margin、left、top、border 等等。
  • 最容易被忽略的操做:獲取一些須要經過即時計算獲得的屬性,當你要用到像這樣的屬性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 時,瀏覽器爲了獲取這些值,也會進行迴流。
  • 當咱們調用了 getComputedStyle 方法,或者 IE 裏的 currentStyle 時,也會觸發迴流。原理是同樣的,都爲求一個「即時性」和「準確性」。
避免方式:
  1. 避免逐條改變樣式,使用類名去合併樣式
  2. 將 DOM 「離線」,使用DocumentFragment
  3. 提高爲合成層,如使用will-change
#divId {
  will-change: transform;
}
複製代碼

優勢網絡

  • 合成層的位圖,會交由 GPU 合成,比 CPU 處理要快
  • 當須要 repaint 時,只須要 repaint 自己,不會影響到其餘的層
  • 對於 transform 和 opacity 效果,不會觸發 layout 和 paint

注意:

部分瀏覽器緩存了一個 flush 隊列,把咱們觸發的迴流與重繪任務都塞進去,待到隊列裏的任務多起來、或者達到了必定的時間間隔,或者「不得已」的時候,再將這些任務一口氣出隊。可是當咱們訪問一些即便屬性時,瀏覽器會爲了得到此時此刻的、最準確的屬性值,而提早將 flush 隊列的任務出隊。

3.渲染引擎什麼狀況下才會爲特定的節點建立新的圖層?

層疊上下文是HTML元素的三維概念,這些HTML元素在一條假想的相對於面向(電腦屏幕的)視窗或者網頁的用戶的z軸上延伸,HTML元素依據其自身屬性按照優先級順序佔用層疊上下文的空間。

  1. 擁有層疊上下文屬性的元素會被提高爲單獨的一層。

擁有層疊上下文屬性:

  • 根元素 (HTML),
  • z-index 值不爲 "auto"的 絕對/相對定位元素,
  • position,固定(fixed) / 沾滯(sticky)定位(沾滯定位適配全部移動設備上的瀏覽器,但老的桌面瀏覽器不支持)
  • z-index值不爲 "auto"的 flex 子項 (flex item),即:父元素 display: flex|inline-flex,
  • z-index值不爲"auto"的grid子項,即:父元素display:grid
  • opacity 屬性值小於 1 的元素(參考 the specification for opacity),
  • transform 屬性值不爲 "none"的元素,
  • mix-blend-mode 屬性值不爲 "normal"的元素,
  • filter值不爲"none"的元素,
  • perspective值不爲"none"的元素,
  • clip-path值不爲"none"的元素
  • mask / mask-image / mask-border不爲"none"的元素
  • isolation 屬性被設置爲 "isolate"的元素
  • 在 will-change 中指定了任意CSS屬性(參考 這篇文章
  • -webkit-overflow-scrolling 屬性被設置 "touch"的元素
  • contain屬性值爲"layout","paint",或者綜合值好比"strict","content"
  1. 須要剪裁(clip)的地方也會被建立爲圖層。

這裏的剪裁指的是,假如咱們把 div 的大小限定爲 200 * 200 像素,而 div 裏面的文字內容比較多,文字所顯示的區域確定會超出 200 * 200 的面積,這時候就產生了剪裁,渲染引擎會把裁剪文字內容的一部分用於顯示在 div 區域。出現這種裁剪狀況的時候,渲染引擎會爲文字部分單首創建一個層,若是出現滾動條,滾動條也會被提高爲單獨的層。

4.JavaScript 是如何支持塊級做用域的?

塊級做用域就是經過詞法環境的棧結構來實現的,而變量提高是經過變量環境來實現,經過這二者的結合,JavaScript 引擎也就同時支持了變量提高和塊級做用域了。

詞法環境跟函數上下文,都是經過棧結構實現的。函數內部經過 var 聲明的變量,在編譯階段全都被存放到變量環境(函數上下文)中,而經過let和const申明的變量會被追加到詞法環境中,當這個塊執行結束以後,追加到詞法做用域的內容又會銷燬掉。

舉個例子:

function foo() {
    var test = 1
    let myname= 'LuckyWinty'
    {
        console.log(myname) 
        let myname= 'winty'
    }
    console.log(test,'---',myname) 
}
foo()
//思考一下會輸出什麼?
複製代碼

執行到第一個console.log前的執行上下文是這樣的:

GitHub

從圖中看,第一個console.log理論上應該輸出 undefined。可是語法規定了一個"暫時性死區(TDZ,當進入它的做用域,它不能被訪問(獲取或設置)直到執行到達聲明)",也就是說雖然經過let聲明的變量已經在詞法環境中了,可是在沒有賦值以前,訪問該變量JavaScript引擎就會拋出一個錯誤。

所以,第一個console.log會拋錯,[Uncaught ReferenceError: Cannot access 'myname' before initialization]。拋錯則函數會中斷執行,爲了能讓咱們的代碼繼續分析,咱們先加個 try-catch ,而後繼續分析:

function foo() {
    var test = 1
    let myname= 'LuckyWinty'
    try{
        {
            console.log(myname) 
            let myname= 'winty'
        }
    }catch(ex){
        console.error(ex)
    }
    console.log(test,'---',myname) 
}
foo()
//思考一下會輸出什麼?
複製代碼

執行到第二個console.log前的執行上下文是這樣的:

GitHub

此時,{}塊做用域中的內容已執行完畢,被銷燬掉了。第二個console.log會輸出1 "---" "LuckyWinty"

5. JavaScript 中的數據是如何存儲在內存中的?

在 JavaScript 中,原始類型的賦值會完整複製變量值,而引用類型的賦值是複製引用地址。

在 JavaScript 的執行過程當中, 主要有三種類型內存空間,分別是代碼空間棧空間堆空間。 其中的代碼空間主要是存儲可執行代碼的,原始類型(Number、String、Null、Undefined、Boolean、Symbol、BigInt)的數據值都是直接保存在「棧」中的,引用類型(Object)的值是存放在「堆」中的。所以在棧空間中(執行上下文),原始類型存儲的是變量的值,而引用類型存儲的是其在"堆空間"中的地址,當 JavaScript 須要訪問該數據的時候,是經過棧中的引用地址來訪問的,至關於多了一道轉手流程。

在編譯過程當中,若是 JavaScript 引擎判斷到一個閉包,也會在堆空間建立換一個「closure(fn)」的對象(這是一個內部對象,JavaScript 是沒法訪問的),用來保存閉包中的變量。因此閉包中的變量是存儲在「堆空間」中的。

JavaScript 引擎須要用棧來維護程序執行期間上下文的狀態,若是棧空間大了話,全部的數據都存放在棧空間裏面,那麼會影響到上下文切換的效率,進而又影響到整個程序的執行效率。一般狀況下,棧空間都不會設置太大,主要用來存放一些原始類型的小數據。而引用類型的數據佔用的空間都比較大,因此這一類數據會被存放到堆中,堆空間很大,能存放不少大的數據,不過缺點是分配內存和回收內存都會佔用必定的時間。所以須要「棧」和「堆」兩種空間。

參考資料

  • 極客時間《瀏覽器工做原理與實踐》

最後

  • 整理不易,麻煩各位給我點個贊啦~~
  • 瞭解更多內容,歡迎關注個人blog, 給我個star~~
  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習~~
  • 以爲內容有幫助能夠關注下個人公衆號 「前端Q」,一塊兒學習成長~~
    GitHub
相關文章
相關標籤/搜索