我自己的理解
虚拟dom:就是 通过js对象表示的DOM结构。
尝试虚拟dom的原因:操作一次的dom的渲染成本远远超过与操作虚拟dom的成本,操作dom是最耗费性能的,所以通过将dom对比操作放在js层,提高效率,也就是虚拟dom。
下面我们通过控制台打印来看一下,一个dom元素的复杂程度。
var div=document.createElement('div')
var result=''
for(let item in div){
result+='|'+item;
}
console.log(result)
//|align|title|lang|translate|dir|dataset|hidden|tabIndex|accessKey|draggable
|spellcheck|autocapitalize|contentEditable|isContentEditable|offsetParent
|offsetTop|offsetLeft|offsetWidth|offsetHeight|style|innerText|outerText
|onabort|onblur|oncancel|oncanplay|oncanplaythrough|onchange||onclose
|oncontextmenu|oncuechange|ondblclick|ondrag|ondragend|ondragenter|ondragleave
|ondragover|ondragstart|ondrop|ondurationchange|onemptied|onended||onfocus
|oninput|oninvalid|onkeydown|onkeypress|onkeyup||eddata|edmetadata
|start|onmousedown|onmouseenter|onmouseleave|onmousemove|onmouseout|onmouseover
|onmouseup|onmousewheel|onpause|onplay|onplaying|onprogress|onratechange|onreset
|onresize|onscroll|onseeked|onseeking|onselect|onstalled|onsubmit|onsuspend
|ontimeupdate|ontoggle|onvolumechange|onwaiting|onwheel|onauxclick|ongotpointercapture|
onlostpointercapture|onpointerdown|onpointermove|onpointerup|onpointercancel|
onpointerover|onpointerout|onpointerenter|onpointerleave|nonce|click|focus|blur|
inputMode|namespaceURI|prefix|localName|tagName|id|className|classList|slot|attributes|
shadowRoot|assignedSlot|innerHTML|outerHTML|scrollTop|scrollLeft|scrollWidth|
scrollHeight|clientTop|clientLeft|clientWidth|clientHeight|onbeforecopy|onbeforecut|
onbeforepaste|oncopy|oncut|onpaste|onsearch|onselectstart|previousElementSibling|
nextElementSibling|children|firstElementChild|lastElementChild|childElementCount|
onwebkitfullscreenchange|onwebkitfullscreenerror|setPointerCapture|releasePointerCapture|
hasPointerCapture|hasAttributes|getAttributeNames|getAttribute|getAttributeNS|setAttribute|
setAttributeNS|removeAttribute|removeAttributeNS|hasAttribute|hasAttributeNS|getAttributeNode|
getAttributeNodeNS|setAttributeNode|setAttributeNodeNS|removeAttributeNode|closest|matches|
webkitMatchesSelector|attachShadow|getElementsByTagName|getElementsByTagNameNS|getElementsByClassName|
insertAdjacentElement|insertAdjacentText|insertAdjacentHTML|requestPointerLock|getClientRects|
getBoundingClientRect|scrollIntoView|scrollIntoViewIfNeeded|animate|before|after|replaceWith|remove|
prepend|append|querySelector|querySelectorAll|webkitRequestFullScreen|webkitRequestFullscreen|attributeStyleMap|
scroll|scrollTo|scrollBy|createShadowRoot|getDestinationInsertionPoints|computedStyleMap|ELEMENT_NODE|ATTRIBUTE_NODE|
TEXT_NODE|CDATA_SECTION_NODE|ENTITY_REFERENCE_NODE|ENTITY_NODE|PROCESSING_INSTRUCTION_NODE|COMMENT_NODE|
DOCUMENT_NODE|DOCUMENT_TYPE_NODE|DOCUMENT_FRAGMENT_NODE|NOTATION_NODE|DOCUMENT_POSITION_DISCONNECTED|
DOCUMENT_POSITION_PRECEDING|DOCUMENT_POSITION_FOLLOWING|DOCUMENT_POSITION_CONTAINS|
DOCUMENT_POSITION_CONTAINED_BY|DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC|nodeType|nodeName
|baseURI|isConnected|ownerDocument|parentNode|parentElement|childNodes|firstChild|lastChild
|previousSibling|nextSibling|nodeValue|textContent|hasChildNodes|getRootNode|normalize
|cloneNode|isEqualNode|isSameNode|compareDocumentPosition|contains|lookupPrefix|
lookupNamespaceURI|isDefaultNamespace|insertBefore|appendChild|replaceChild|removeChild|addEventListener|removeEventListener|dispatchEvent光是第一层外层的属性打印出来就如此之多,可想而知,对比虚拟dom的几个属性,dom的操作比虚拟dom 所耗费的性能多得多。
DOM 操作是“昂贵”的,js 运行效率高,所以我们尽量减少 DOM 操作,而不是“推倒重来”,项目越复杂,影响就越严重,所以使用 vdom 即可解决这个问题
下面是html代码例子以及对应的虚拟dom结构。
html代码:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
对应的虚拟dom形式:
{
tagName: 'ul', // 节点标签名
props: { // DOM的属性,用一个对象存储键值对
id: '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"]},
]
}Virtual DOM 算法:包括几个步骤:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
根据vdom的描述,下面使用jQuery来模拟实现上面的代码
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)但是这样每次也是重新渲染全部的dom元素,我们要的效果是将需要改变的元素进行更新,其他的保持不变,所以要进行优化,所以下面我们使用snabbdom进行优化。snabbdom使用的diff 算法进行比对,找出本次 DOM 必须更新的节点来更新,其他的不更新,从而优化渲染性能。
更多api请移步githubSnabbdom ( A virtual DOM library)
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) {
// re-render
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)
})使用控制台,调试下看到,点击change按钮进行改变,只是改变对应的元素而已,其他元素没有发生变化。
Virtual DOM 算法:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了
snabbdom使用diff 算法进行比对,找出本次 DOM 必须更新的节点来更新,其他的不更新,从而优化渲染性能。
vdom diff算法最重要的两个api,一个是patch(container,vnode);一个是patch(vnode,newnode);
下面我们通过js代码简单地模拟下两个实现,这是假设所有children都是tag的情况,当然实际情况比这复杂多了。
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
}
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)
// 替换
}diff算法其实是linux的基础命令,vdom中应用diff算法是为了找出需要更新的节点,vdom的模拟简单实现有两个重要的api patch(container,vnode);一个是patch(vnode,newnode);