1. 瀏覽器渲染過程是怎樣的?
按照渲染的時間順序,流水線可分為如下幾個子階段:構建 DOM 樹
、樣式計算
、布局階段
、分層
、柵格化
和顯示
。
- 渲染進程將 HTML 內容轉換為能夠讀懂DOM 樹結構。
- 渲染引擎將 CSS 樣式表轉化為瀏覽器可以理解的styleSheets,計算出 DOM 節點的樣式。
- 創建布局樹,並計算元素的布局信息。
- 對布局樹進行分層,並生成分層樹。
- 為每個圖層生成繪制列表,並將其提交到合成線程。合成線程將圖層分圖塊,並柵格化將圖塊轉換成位圖。
- 合成線程發送繪制圖塊命令給瀏覽器進程。瀏覽器進程根據指令生成頁面,並顯示到顯示器上。
瀏覽器從網絡或硬盤中獲得HTML字節數據后會經過一個流程將字節解析為DOM樹,先將HTML的原始字節數據轉換為文件指定編碼的字符,然后瀏覽器會根據HTML規范來將字符串轉換成各種令牌標簽,如html、body等。最終解析成一個樹狀的對象模型,就是dom樹;
獲取css,獲取style標簽內的css、或者內嵌的css,或者當HTML代碼遇見 標簽時,瀏覽器會發送請求獲得該標簽中標記的CSS,當渲染引擎接收到 CSS 文本時,會執行一個轉換操作,將 CSS 文本轉換為瀏覽器可以理解的styleSheets
創建布局樹,遍歷 DOM 樹中的所有可見節點,並把這些節點加到布局中;而不可見的節點會被布局樹忽略掉,如 head 標簽下面的全部內容,再比如 body.p.span 這個元素,因為它的屬性包含 dispaly:none,所以這個元素也沒有被包進布局樹。最后計算 DOM 元素的布局信息,使其都保存在布局樹中。布局完成過程中,如果有js操作或者其他操作,對元素的顏色,背景等作出改變就會引起重繪,如果有對元素的大小、定位等有改變則會引起回流。
因為頁面中有很多復雜的效果,如一些復雜的 3D 變換、頁面滾動,或者使用 z-indexing 做 z 軸排序等,為了更加方便地實現這些效果,渲染引擎還需要為特定的節點生成專用的圖層,並生成一棵對應的圖層樹。
渲染引擎實現圖層的繪制,把一個圖層的繪制拆分成很多小的繪制指令然后再把這些指令按照順序組成一個待繪制列表,當圖層的繪制列表准備好之后,主線程會把該繪制列表提交給合成線程,合成線程會將圖層划分為圖塊,然后按照視口附近的圖塊來優先生成位圖(實際生成位圖的操作是由柵格化來執行的。所謂柵格化,是指將圖塊轉換為位圖)
一旦所有圖塊都被光柵化,合成線程就會生成一個繪制圖塊的命令,然后將該命令提交給瀏覽器進程,瀏覽器最后進行顯示。
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 時,也會觸發回流。原理是一樣的,都為求一個“即時性”和“准確性”。
避免方式:
- 避免逐條改變樣式,使用類名去合並樣式
- 將 DOM “離線”,使用DocumentFragment
- 提升為合成層,如使用
will-change
#divId {
will-change: transform;
}
優點
- 合成層的位圖,會交由 GPU 合成,比 CPU 處理要快
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
- 對於 transform 和 opacity 效果,不會觸發 layout 和 paint
注意:
部分瀏覽器緩存了一個 flush 隊列,把我們觸發的回流與重繪任務都塞進去,待到隊列里的任務多起來、或者達到了一定的時間間隔,或者“不得已”的時候,再將這些任務一口氣出隊。但是當我們訪問一些即使屬性時,瀏覽器會為了獲得此時此刻的、最准確的屬性值,而提前將 flush 隊列的任務出隊。
3.渲染引擎什么情況下才會為特定的節點創建新的圖層?
層疊上下文
是HTML元素的三維概念,這些HTML元素在一條假想的相對於面向(電腦屏幕的)視窗或者網頁的用戶的z軸上延伸,HTML元素依據其自身屬性按照優先級順序占用層疊上下文的空間。
- 擁有層疊上下文屬性的元素會被提升為單獨的一層。
擁有層疊上下文屬性:
- 根元素 (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"
- 需要剪裁(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
前的執行上下文是這樣的:
從圖中看,第一個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
前的執行上下文是這樣的:
此時,{}
塊作用域中的內容已執行完畢,被銷毀掉了。第二個console.log
會輸出1 "---" "LuckyWinty"
。
5. JavaScript 中的數據是如何存儲在內存中的?
在 JavaScript 中,原始類型的賦值會完整復制變量值,而引用類型的賦值是復制引用地址。
在 JavaScript 的執行過程中, 主要有三種類型內存空間,分別是代碼空間
、棧空間
、堆空間
。
其中的代碼空間主要是存儲可執行代碼的,原始類型(Number、String、Null、Undefined、Boolean、Symbol、BigInt)的數據值都是直接保存在“棧”中的,引用類型(Object)的值是存放在“堆”中的。因此在棧空間中(執行上下文),原始類型存儲的是變量的值,而引用類型存儲的是其在"堆空間"中的地址,當 JavaScript 需要訪問該數據的時候,是通過棧中的引用地址來訪問的,相當於多了一道轉手流程。
在編譯過程中,如果 JavaScript 引擎判斷到一個閉包,也會在堆空間創建換一個“closure(fn)”
的對象(這是一個內部對象,JavaScript 是無法訪問的),用來保存閉包中的變量。所以閉包中的變量是存儲在“堆空間”中的。
JavaScript 引擎需要用棧來維護程序執行期間上下文的狀態,如果棧空間大了話,所有的數據都存放在棧空間里面,那么會影響到上下文切換的效率,進而又影響到整個程序的執行效率。通常情況下,棧空間都不會設置太大,主要用來存放一些原始類型的小數據。而引用類型的數據占用的空間都比較大,所以這一類數據會被存放到堆中,堆空間很大,能存放很多大的數據,不過缺點是分配內存和回收內存都會占用一定的時間。因此需要“棧”和“堆”兩種空間。
參考資料
- 極客時間《瀏覽器工作原理與實踐》
最后
- 歡迎加我微信(winty230),拉你進技術群,長期交流學習...
- 歡迎關注「前端Q」,認真學前端,做個有態度的技術人...