浅谈虚拟dom
我自己的理解
虚拟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);
共同学习,写下你的评论
评论加载中...
作者其他优质文章