在聊virtual DOM前咱們要先來講說瀏覽器的渲染流程.javascript
瀏覽器如何渲染頁面css
做爲一名web前端碼農,天天都在接觸着瀏覽器.久而久之咱們都會有疑惑,瀏覽器是怎麼解析咱們的代碼而後渲染的呢?弄明白瀏覽器的渲染原理,對於咱們平常前端開發中的性能優化有重要意義。html
因此今天咱們來給你們詳細說說瀏覽器是怎麼渲染DOM的。前端
瀏覽器渲染大體流程java
首先,瀏覽器會經過請求的 URL 進行域名解析,向服務器發起請求,接收資源(HTML、CSS、JS、Images)等等,那麼以後瀏覽器又會進行如下解析:node
而解析完以上步驟後, 瀏覽器會經過DOM Tree 和CSS Rule Tree來構建 Render Tree(渲染樹)。git
根據渲染樹來佈局,以計算每一個節點的幾何信息。github
最後將各個節點繪製到頁面上。web
HTML解析算法
<html>
<html>
<head>
<title>Web page parsing</title>
</head>
<body>
<div>
<h1>Web page parsing</h1>
<p class="text">This is an example Web page.</p>
</div>
</body>
</html>
複製代碼
那麼解析的DOM樹就是如下這樣
CSS解析
/* rule 1 */ div { display: block; text-indent: 1em; }
/* rule 2 */ h1 { display: block; font-size: 3em; }
/* rule 3 */ p { display: block; }
/* rule 4 */ [class="text"] { font-style: italic; }
複製代碼
CSS Rule Tree會比照着DOM樹來對應生成,在這裏須要注意的就是CSS匹配DOM的規則。不少人都覺得CSS匹配DOM樹的速度會很快,其實否則。
樣式系統從最右邊的選擇符開始向左側移動來匹配一條規則。樣式系統會一直向左匹配選擇符直到規則匹配完畢或者因爲出錯中止匹配.
這裏就衍生出一個問題,爲何解析CSS的時候選擇從右往左呢?
爲了匹配效率。
全部樣式規則極有可能數量很大,並且絕大多數不會匹配到當前的 DOM 元素,因此有一個快速的方法來判斷「這個 selector 不匹配當前元素」就是極其重要的。
若是正向解析,例如「div div p em」,咱們首先就要檢查當前元素到 html 的整條路徑,找到最上層的 div,再往下找,若是遇到不匹配就必須回到最上層那個 div,往下再去匹配選擇器中的第一個 div,回溯若干次才能肯定匹配與否,效率很低。
能夠看如下的例子:
<div>
<div class="jartto">
<p><span> 111 </span></p>
<p><span> 222 </span></p>
<p><span> 333 </span></p>
<p><span class='yellow'> 444 </span></p>
</div>
</div>
<div>
<div class="jartto1">
<p><span> 111 </span></p>
<p><span> 222 </span></p>
<p><span> 333 </span></p>
<p><span class='red'> 555 </span></p>
</div>
</div>
div > div.jartto p span.yellow{
color:yellow;
}
複製代碼
對於上述例子,若是按從左到右的方式進行查找:
1.先找到全部 div 節點;
2.在 div 節點內找到全部的子 div ,而且是 class = 「jartto」
3.而後再依次匹配 p span.yellow 等狀況;
4.遇到不匹配的狀況,就必須回溯到一開始搜索的 div 或者 p 節點,而後去搜索下個節點,重複這樣的過程。
試想一下,若是採用從左至右的方式讀取 CSS 規則,那麼大多數規則讀到最後(最右)纔會發現是不匹配的,這樣會作費時耗能,最後有不少都是無用的;而若是採起從右向左的方式,那麼只要發現最右邊選擇器不匹配,就能夠直接捨棄了,避免了許多無效匹配。
因此瀏覽器 CSS 匹配核心算法的規則是以從右向左方式匹配節點的。這樣作是爲了減小無效匹配次數,從而匹配快、性能更優。
CSS匹配HTML元素是一個至關複雜和有性能問題的事情。因此,你就會在N多地方看到不少人都告訴你,DOM樹要小,CSS儘可能用id和class,千萬不要過渡層疊下去,……
構建渲染樹
經運行過Javascript腳本後解析出了最終的DOM Tree 和 CSS Rule Tree, 根據這二者,就能合成咱們的Render Tree,網羅網頁上全部可見的 DOM 內容,以及每一個節點的全部 CSSOM 樣式信息。
爲構建渲染樹,瀏覽器大致上完成了下列工做:
渲染的注意事項
在這裏要說下兩個概念,一個是repaint和reflow,這兩個是影響瀏覽器渲染的主要緣由:
咱們來看一段javascript代碼:
var bstyle = document.body.style; // cache
bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // 再一次的 reflow 和 repaint
bstyle.color = "blue"; // repaint
bstyle.backgroundColor = "#fad"; // repaint
bstyle.fontSize = "2em"; // reflow, repaint
// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));
複製代碼
固然,咱們的瀏覽器是聰明的,它不會像上面那樣,你每改一次樣式,它就reflow或repaint一次。通常來講,瀏覽器會把這樣的操做積攢一批,而後作一次reflow,這又叫異步reflow或增量異步reflow。
雖然瀏覽器會幫咱們優化reflow的操做,但在實際開發過程當中,咱們仍是得經過幾種方法去減小reflow的操做
減小reflow/repaint的方法
不要一條一條地修改DOM的樣式。與其這樣,還不如預先定義好css的class,而後修改DOM的className。
// bad var left = 10, top = 10; el.style.left = left + "px"; el.style.top = top + "px";
// Good el.className += " theclassname";
// Good el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
2)把DOM離線後修改。如:
3)不要把DOM結點的屬性值放在一個循環裏當成循環裏的變量。否則這會致使大量地讀寫這個結點的屬性。
4)千萬不要使用table佈局。由於可能很小的一個小改動會形成整個table的從新佈局。
5)儘量的修改層級比較低的DOM。固然,改變層級比較底的DOM有可能會形成大面積的reflow,可是也可能影響範圍很小。
Virtual DOM
Virtual DOM是什麼?
大部分前端開發者對Virtual DOM這個詞都很熟悉了,簡單來說,Virtual DOM就是在數據和真實 DOM 之間創建了一層緩衝層。當數據變化觸發渲染後,並不直接更新到DOM上,而是先生成 Virtual DOM,與上一次渲染獲得的 Virtual DOM 進行比對,在渲染獲得的 Virtual DOM 上發現變化,而後將變化的地方更新到真實 DOM 上。
複製代碼
爲何說Virtual DOM快?
1)DOM結構複雜,操做很慢
咱們在控制檯輸入
var div = document.createElement('div')
var str = ''
for (var key in div) {
str = str + key + "\n"
}
console.log(str)
複製代碼
能夠很容易發現,咱們的一個空div對象,他的屬性就有幾百個,因此說DOM的操做慢是能夠理解的。不是瀏覽器不想好好實現DOM,而是DOM設計得太複雜,沒辦法。
2)JS計算很快
Julia有一個Benchmark,Julia Benchmarks, 能夠看到Javascript跟C語言很接近了,也就幾倍的差距,跟Java基本也是一個量級。 這就說明,單純的Javascript運行起來其實速度是很快的。
而相對於DOM,咱們原生的JavaScript對象處理起來則會更快更簡單.
咱們經過JavaScript,能夠很容易的用JavaScript對象表示出來.
var olE = {
tagName: 'ul', // 標籤名
props: { // 屬性用對象存儲鍵值對
id: 'ul-list',
class: 'list'
},
children: [ // 子節點
{tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
{tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
]
}
複製代碼
對應的HTML寫法:
<ul id='ol-list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
複製代碼
那麼,既然咱們能夠用javascript來表示DOM,那麼表明咱們能夠用JavaScript來構造咱們的真實DOM樹,當咱們的DOM樹須要更新了,那咱們先渲染更改這個JavaScript構造的Virtual DOM樹,再更新到真實DOM樹上。
因此Virtual DOM算法就是:
一開始先用 JavaScript 對象結構表示 DOM 樹的結構;而後用這個樹構建一個真正的 DOM 樹,插到文
檔當中。當狀態變動時,從新構造一棵新的對象樹。而後用新的樹和舊的樹進行比較兩個樹的差別。
而後把差別更新到舊的樹上,最後再把整個變動寫入真實 DOM。
簡單Virtual DOM 算法實現
步驟一:用JS對象模擬DOM樹,並構建
用 JavaScript 來表示一個 DOM 節點是很簡單的事情,你只須要記錄它的節點類型、屬性,還有子節點:
// 建立虛擬DOM函數
function Element (tagName, props, children) {
this.tagName = tagName // 標籤名
this.props = props // 對應屬性(如ID、Class)
this.children = children // 子元素
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
複製代碼
實際應用以下:
var el = require('./element')
// 普通ul和li對象就能夠表示爲這樣
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])
複製代碼
如今ul只是一個 JavaScript 對象表示的 DOM 結構,頁面上並無這個結構。咱們能夠根據這個ul構建真正的
// 構建真實DOM函數
Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根據tagName構建
var props = this.props
for (var propName in props) { // 設置節點的DOM屬性
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點
: document.createTextNode(child) // 若是字符串,只構建文本節點
el.appendChild(childEl)
})
return el
}
複製代碼
咱們的render方法會根據tagName去構建一個真實的DOM節點,設置節點屬性,再遞歸到子元素構建:
var ulRoot = ul.render() // 將js構建的dom對象傳給render構建
document.body.appendChild(ulRoot) // 真實的DOM對象塞入body
複製代碼
這樣咱們body中就有了ul和li的DOM元素了
<body>
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
</body>
複製代碼
步驟二:比較兩棵虛擬DOM樹的差別
在這裏咱們假設對咱們修改了某個狀態或者某個數據,這就會產生新的虛擬DOM
// 新DOM
var ol = el('ol', {id: 'ol-list'}, [
el('li', {class: 'ol-item'}, ['Item 1']),
el('li', {class: 'ol-item'}, ['Item 2']),
el('li', {class: 'ol-item'}, ['Item 3']),
el('li', {class: 'ol-item'}, ['Item 4'])
])
// 舊DOM
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 3']),
el('li', {class: 'item'}, ['Item 2'])
])
複製代碼
那麼咱們會和先和,剛剛上一次生成的虛擬DOM樹進行比對.
咱們應該都很清楚,virtual DOM算法的核心部分,就在比較差別這一部分,也就是所謂的 diff算法。
由於不多出現跨層級的移動。
diff算法通常來講,都是同一層級比對同一層級的
var patch = {
'REPLACE' : 0, // 替換
'REORDER' : 1, // 新增、刪除、移動
'PROPS' : 2, // 屬性更改
'TEXT' : 3 // 文本內容更改
}
複製代碼
例如,上面的div和新的div有差別,當前的標記是0,那麼:
// 用數組存儲新舊節點的不一樣
patches = [
// 每一個數組表示一個元素的差別
[
{difference},
{difference}
],
[
{difference},
{difference}
]
]
patches[0] = [
{
type: REPALCE,
node: newNode // el('section', props, children)
},
{
type: PROPS,
props: {
id: "container"
}
},
{
type: REORDER,
moves: [
{index: 2, item: item, type: 1}, // 保留的節點
{index: 0, type: 0}, // 該節點被刪除
{index: 1, item: item, type: 1} // 保留的節點
]
}
];
若是是文本節點內容更改,就記錄下:
patches[2] = [{
type: TEXT,
content: "我是新修改的文本內容"
}]
// 詳細算法查看diff.js
複製代碼
每種差別都會有不一樣的對比方式,經過比對後會將差別記錄下來,應用到真實DOM上,並把最近最新的虛擬DOM樹保存下來,以便下次比對使用。
步驟三:把差別應用到真正的DOM樹上
經過比對後,咱們已經知道了,差別的節點是哪些,咱們能夠方便對真實DOM作最小化的修改。
// 詳情看patch.js
複製代碼
發現問題
到這裏咱們發現一個問題,不是說 Virtual DOM更快嗎? 但是最終你仍是要進行DOM操做呀?那意義何在?還不如一開始咱們就直接進行DOM操做來的方便。
因此到這裏咱們要對Virtual DOM 有一個正確的認識
網上都說操做真實 DOM 慢,但測試結果卻比 React 更快,爲何?
chrisharrington.github.io/demos/perfo…
最優更改
Virtual DOM的算法可以向你保證的就是,每一次的DOM操做我都能達到算法上的理論最優,而若是是你本身去操做DOM,這並不能保證。
其次
開發模式的更改
爲了讓開發者把精力集中在操做數據,而非接管 DOM 操做。Virtual DOM能讓咱們在實際開發過程當中,不須要去理會複雜的DOM結構,而只需理會綁定DOM結構的狀態和數據便可,這從開發上來講 就是一個很大的進步