为了账号安全,请及时绑定邮箱和手机立即绑定

浅谈虚拟dom

标签:
JavaScript

我自己的理解


虚拟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);


点击查看更多内容
3人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消