目前最流行的兩大前端框架,React 和 Vue,都不約而同的藉助 Virtual DOM 技術提升頁面的渲染效率。那麼,什麼是 Virtual DOM ?它是經過什麼方式去提高頁面渲染效率的呢?本系列文章會詳細講解 Virtual DOM 的建立過程,並實現一個簡單的 Diff 算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的 Virtual DOM 。敲單詞太累了,下文 Virtual DOM 一概用 VD 表示。javascript
這是 VD 系列文章的開篇,後續還會有更多的文章帶你深刻了解 VD 的奧祕。前端
本質上來講,VD 只是一個簡單的JS對象,而且最少包含tag
、props
和children
三個屬性。不一樣的框架對這三個屬性的命名會有點差異,但表達的意思是一致的。它們分別是標籤名( tag )、屬性( props )和子元素對象( children )。下面是一個典型的 VD 對象例子:java
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
複製代碼
VD 跟 dom 對象有一一對應的關係,上面的 VD 是由如下的 HTML 生成的react
<div>
Hello World
<ul>
<li id="1" class="li-1">
第1
</li>
</ul>
</div>
複製代碼
一個 dom 對象,好比li
,由tag(li)
, props({id: 1, class: "li-1"})
和children(["第", 1])
三個屬性來描述。git
藉助 VD,能夠達到有效減小頁面渲染次數的目的,從而提升渲染效率。咱們先來看下頁面的更新通常會通過幾個階段。github
從上面的例子中,能夠看出頁面的呈現會分如下3個階段:算法
這個例子裏面,JS 計算用了691
毫秒,生成渲染樹578
毫秒,繪製73
毫秒。若是能有效的減小生成渲染樹和繪製所花的時間,更新頁面的效率也會隨之提升。 經過 VD 的比較,咱們能夠將多個操做合併成一個批量的操做,從而減小 dom 重排的次數,進而縮短了生成渲染樹和繪製所花的時間。至於如何基於 VD 更有效率的更新 dom,是一個頗有趣的話題,往後有機會將另寫一篇文章介紹。數組
咱們先從如何生成 VD 提及。藉助 JSX 編譯器,能夠將文件中的 HTML 轉化成函數的形式,而後再利用這個函數生成 VD。看下面這個例子:前端框架
function render() {
return (
<div> Hello World <ul> <li id="1" class="li-1"> 第1 </li> </ul> </div>
);
}
複製代碼
這個函數通過 JSX 編譯後,會輸出下面的內容:babel
function render() {
return h(
'div',
null,
'Hello World',
h(
'ul',
null,
h(
'li',
{ id: '1', 'class': 'li-1' },
'\u7B2C1'
)
)
);
}
複製代碼
這裏的h是一個函數,能夠起任意的名字。這個名字經過 babel 進行配置:
// .babelrc 文件
{
"plugins": [
["transform-react-jsx", {
"pragma": "h" // 這裏可配置任意的名稱
}]
]
}
複製代碼
接下來,咱們只須要定義 h 函數,就能構造出 VD
function flatten(arr) {
return [].concat.apply([], arr);
}
function h(tag, props, ...children) {
return {
tag,
props: props || {},
children: flatten(children) || []
};
}
複製代碼
h 函數會傳入三個或以上的參數,前兩個參數一個是標籤名,一個是屬性對象,從第三個參數開始的其它參數都是 children。children 元素有多是數組的形式,須要將數組解構一層。好比:
function render() {
return (
<ul> <li>0</li> { [1, 2, 3].map( i => ( <li>{i}</li> )) } </ul>
);
}
// JSX 編譯後
function render() {
return h(
'ul',
null,
h(
'li',
null,
'0'
),
/* * 須要將下面這個數組解構出來再放到 children 數組中 */
[1, 2, 3].map(i => h(
'li',
null,
i
))
);
}
複製代碼
繼續以前的例子。執行 h 函數後,最終會獲得以下的 VD 對象:
{
tag: "div",
props: {},
children: [
"Hello World",
{
tag: "ul",
props: {},
children: [{
tag: "li",
props: {
id: 1,
class: "li-1"
},
children: ["第", 1]
}]
}
]
}
複製代碼
下一步,經過遍歷 VD 對象,生成真實的 dom
// 建立 dom 元素
function createElement(vdom) {
// 若是 vdom 是字符串或者數字類型,則建立文本節點,好比「Hello World」
if (typeof vdom === 'string' || typeof vdom === 'number') {
return doc.createTextNode(vdom);
}
const {tag, props, children} = vdom;
// 1. 建立元素
const element = doc.createElement(tag);
// 2. 屬性賦值
setProps(element, props);
// 3. 建立子元素
// appendChild 在執行的時候,會檢查當前的 this 是否是 dom 對象,所以要 bind 一下
children.map(createElement)
.forEach(element.appendChild.bind(element));
return element;
}
// 屬性賦值
function setProps(element, props) {
for (let key in props) {
element.setAttribute(key, props[key]);
}
}
複製代碼
createElement
函數執行完後,dom元素就建立完並展現到頁面上了(頁面比較醜,不要介意...)。
本文介紹了 VD 的基本概念,並講解了如何利用 JSX 編譯 HTML 標籤,而後生成 VD,進而建立真實 dom 的過程。下一篇文章將會實現一個簡單的 VD Diff 算法,找出 2 個 VD 的差別並將更新的元素映射到 dom 中去。
P.S.: 想看完整代碼見這裏:代碼