Virtual Dom 是虛擬DOM,咱們用JS來模擬DOM結構,結構相似下面的代碼:javascript
{
tag:'ul',
attrs:{
id:'list'
}
children:[
{
tag:'li',
attrs:{className:'item'},
children:['item 1']
},
{ tag:'li',
attrs:{className:'item'},
children:['item 2']
}
]
}複製代碼
以上代碼模擬的就是這樣的DOM結構css
<ul>
<li class='item'>item 1</li>
<li class='item'>item 2</li></ul>複製代碼
那麼爲何會有VDOM(virtual dom簡稱)這樣的結構呢?html
咱們來模擬這樣的一個場景需求。vue
1.有一堆數據,須要將數據渲染成表格java
2.隨便修改一個信息,表格也會跟着變化node
若是沒有VDOM,咱們會用這樣的代碼來完成需求jquery
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title></head><body> <div id="container"></div> <button id="btn-change">change</button> <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script type="text/javascript"> var data = [ { name: '張三', age: '20', address: '北京' }, { name: '李四', age: '21', address: '上海' }, { name: '王五', age: '22', address: '廣州' } ] // 渲染函數 function render(data) { var $container = $('#container') // 清空容器,重要!!! $container.html('') // 拼接 table var $table = $('<table>') $table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>')) data.forEach(function (item) { $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>')) }) // 渲染到頁面 $container.append($table) } $('#btn-change').click(function () { data[1].age = 30 data[2].address = '深圳' // re-render 再次渲染 render(data) }) // 頁面加載完馬上執行(初次渲染) render(data) </script> </body> </html> 複製代碼
上面的代碼雖然完成了需求,可是,遺憾的是,若是我只是修改一部分數據,整個table算法
都須要所有渲染。對於瀏覽器而言,渲染DOM是一個很是「昂貴「的過程。那麼,有沒有什麼辦法,修改部分數據的時候,只是渲染我修改的DOM呢?瀏覽器
咱們首先來使用一下snabbdom這個庫,它會利用VDOM來實現局部渲染。一塊兒來感覺一下bash
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Document</title></head><body> <div id="container"></div> <button id="btn-change">change</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script> <script type="text/javascript"> var snabbdom = window.snabbdom // 定義關鍵函數 patch var patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]) // 定義關鍵函數 h var h = snabbdom.h // 原始數據 var data = [ { name: '張三', age: '20', address: '北京' }, { name: '李四', age: '21', address: '上海' }, { name: '王五', age: '22', address: '廣州' } ] // 把表頭也放在 data 中 data.unshift({ name: '姓名', age: '年齡', address: '地址' }) var container = document.getElementById('container') // 渲染函數 var vnode function render(data) { var newVnode = h('table', {}, data.map(function (item) { var tds = [] var i for (i in item) { if (item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')) } } return h('tr', {}, tds) })) if (vnode) { // 若是已經渲染了 patch(vnode, newVnode) } else { // 初次渲染 patch(container, newVnode) } // 存儲當前的 vnode 結果 vnode = newVnode } // 初次渲染 render(data) var btnChange = document.getElementById('btn-change') btnChange.addEventListener('click', function () { data[1].age = 30 data[2].address = '深圳' // re-render render(data) }) </script> </body> </html>複製代碼
以上代碼,則實現了當你修改了部分數據時,只渲染一部分數據。
那麼,以上代碼的核心就是兩個函數,咱們須要對此來作探討。一個是函數h,一個是函數patch
函數h返回的值是一個vnode,也就是虛擬DOM節點,如圖所示
也就是說使用h函數能夠生成相似於右邊的vnode結構。
那麼關鍵函數patch的做用,則是將vnode渲染成真實的DOM節點,而後塞入到容器裏面。
若是容器裏面已經有生成好的vnode,那麼,則會將新生成的newVnode和以前的vnode相比較,而後將不一樣的節點找出來,而後代替舊的節點。
到如今爲止,已經基本上了解了VDOM的含義以及爲何會用VDOM,咱們來作一個簡單的總結
那麼,接下來又出現了一個問題,咱們如何知道哪一個節點須要更新呢?這就是diff算法的做用
由於diff算法自己太過於複雜,因此只須要理解一下核心的思想便可。
那麼咱們只須要關注在渲染的時候,發生了什麼事情,理解下面這兩個事件的核心流程便可。
也就是說,咱們須要理解的是:
咱們要實現的是這樣的過程:
咱們來模擬一下上面的建立過程,只是僞代碼,咱們瞭解大體的流程
function createElement(vnode) {
var tag = vnode.tag // 'ul'
var attrs = vnode.attrs || {}
var children = vnode.children || []
if (!tag) {
return null
}
// 建立真實的 DOM 元素
var elem = document.createElement(tag)
// 屬性
var attrName
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
// 給 elem 添加屬性
elem.setAttribute(attrName, attrs[attrName])
}
}
// 子元素
children.forEach(function (childVnode) {
// 給 elem 添加子元素,若是還有子節點,則遞歸的生成子節點。
elem.appendChild(createElement(childVnode)) // 遞歸
}) // 返回真實的 DOM 元素
return elem
}複製代碼
那麼,經過上面的模擬代碼,已經能夠很好的瞭解最開始將vdom渲染到容器的過程。
這個過程就是將newVnode和vnode對比,將差別進行渲染的部分。
那麼僞代碼流程以下:
function updateChildren(vnode, newVnode) {
var children = vnode.children || []
var newChildren = newVnode.children || []
children.forEach(function (childVnode, index) {
var newChildVnode = newChildren[index]
if (childVnode.tag === newChildVnode.tag) {
// 深層次對比,遞歸
updateChildren(childVnode, newChildVnode)
} else {
// 替換
replaceNode(childVnode, newChildVnode)
}
}
)}
function replaceNode(vnode, newVnode) {
var elem = vnode.elem // 取得舊的 真實的 DOM 節點
var newElem = createElement(newVnode)//生成新的真實的dom節點
// 替換
}複製代碼
那麼真正的替換過程有哪些呢?簡單的總結一下:
elem
newVnode
和oldVnode
是否指向同一個對象,若是是,那麼直接return
el
的文本節點設置爲Vnode
的文本節點。oldVnode
有子節點而newVnode
沒有,則刪除el
的子節點oldVnode
沒有子節點而newVnode
有,則將Vnode
的子節點真實化以後添加到elem
updateChildren
函數比較子節點,這一步很重要,請參考這篇文章以上只是簡單的理解了diff算法的流程,關於更多的diff算法的詳細過程,能夠閱讀參考文章。