浅谈 Virtual DOM 的那些事

2018-06-27 09:04:07来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

背景

我们都知道频繁的dom给我们带来的代价是昂贵的,例如我们有时候需要去更新Table 的部分数据,必须去重新重绘表格,这代价实在是太大了,相比于频繁的手动去操作dom而带来性能问题,vdom很好的将dom做了一层映射关系,进而将在我们本需要直接进行dom的一系列操作,映射到了操作vdom.

  

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>virtualDom</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">修改</button>
<script type="application/javascript" src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script type="application/javascript">
    const dataSource = [{
        key: '1',
        name: '胡彦斌',
        age: 32,
        address: '西湖区湖底公园1号'
    }, {
        key: '2',
        name: '胡彦祖',
        age: 42,
        address: '西湖区湖底公园1号'
    }];

    const columns = [{
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
    }, {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
    }, {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
    }];
   function render(data) {
       var container = $('#container');
       container.html(''); //清空容器
       //添加表头
       var $table =$('<table>')
       $table.append($('<tr>'))
       columns.map(function(item,index){
           $table.append($('<td>'+item.title+'</td>'))
       })
       $table.append($('</tr>'))
       //添加表体
       dataSource.forEach(function(item){
           $table.append($('<tr></tr><td>'+item.name+'</td>'+'<td>'+item.age+'</td>'+'<td>'+item.address+'</td></tr>'))
       })
       //只渲染一遍dom,尽然如此,还是需要清空容器
       container.append($table)
   }
   $('#btn-change').click(function(){
       dataSource[0].name="胡军网";
       dataSource[1].address='南山区沙河东路1号'
       //re——render
       render(dataSource)
   })
   render()
</script>
</body>
</html>

解决

  1. virtual dom,虚拟 DOM
  2. 用 JS 模拟 DOM

什么是vdom

HTML DOM 结构:

<ul id="ul-list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
    <li class="item">Item 3</li>
</ul>

针对于上面HTML DOM 结构,可以用JS表示为:

var ulE = {
    tagName: 'ul', // 标签名
    props: { // 属性用对象存储键值对
        id: 'ul-list'
    },
    children: [ // 子节点
        {tagName: 'li', props: {className: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {className: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {className: 'item'}, children: ["Item 3"]},
    ]
}

 JS对象中抽取公共的部分属性,进一步封装:

export default Ele = (tagName, props, children) => {
    this.tagName = tagName
    this.props = props
    this.children = children
}  

  

import * as el from 'ele';
var ol = el('ul', {id: 'ul-list'}, [
    el('li', {className: 'item'}, ['Item 1']),
    el('li', {className: 'item'}, ['Item 2']),
    el('li', {className: 'item'}, ['Item 3'])
]);


通过snabbdom进行virtual dom(核心API:h函数、patch函数)

案例一: 对比局部更新添加修改ul中的li

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>snabbdom</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">修改</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.min.js"></script>
<script type="application/javascript">
    var snabbdom = window.snabbdom

    // 定义 patch
    var patch =snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ]);
    // 定义h
    var h =snabbdom.h;
    var container = document.getElementById('container');
    //定义 virtual node
    var vnode = h('ul#ul-list',{},[
        h('li.item',{},'item1'),
        h('li.item',{},'item2')
    ])
    patch(container,vnode);
    document.getElementById('btn-change').addEventListener('click',function () {
        var newVnode = h('ul#ul-list',{},[
            h('li.item',{},'item1'),
            h('li.item',{},'西湖区湖底公园1号'),
            h('li.item',{},'西湖区湖底公园2号'),
            h('li.item',{},'西湖区湖底公园3号')
        ])
        patch(vnode,newVnode);
    })
</script>
</body>
</html>

  item1 所在的li不会进行dom渲染,只有新增或者修改的node才会发生改变,执行结果如下所示:

案例二: 局部更新部分Table 数据(使用Vitual DOM 性能的提升)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>snabbdom</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">修改</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.min.js"></script>
<script type="application/javascript">
    var snabbdom = window.snabbdom

    // 定义 patch
    var patch =snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ]);
    // 定义h
    var h =snabbdom.h;
    var container = document.getElementById('container');
    const dataSource = [{
        key: '1',
        name: '胡彦斌',
        age: 32,
        address: '西湖区湖底公园1号'
    }, {
        key: '2',
        name: '胡彦祖',
        age: 42,
        address: '西湖区湖底公园1号'
    }];

    const columns = [{
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
    }, {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
    }, {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
    }];
    var vdom=null;
    function render(dataSource) {
        var titleTr= [];
        titleTr.push(h('td',{},' '))
        columns.forEach(function(item){
           if(item.hasOwnProperty('title')){
               titleTr.push(h('td',{},item['title']))
           }
        })
        var vTitle = h('tr',{},titleTr);
        var vBody =dataSource.map(function(item){
            const vp= []
            for(var i in item) {
                if(item.hasOwnProperty(i)){
                    vp.push(h('td',{},item[i]))
                }
            }
            return h('tr',{},vp)
        })
        vBody.unshift(vTitle);
        var vTable = Object.assign([],vBody);
        var newVnode = h('table',{},vTable)
        if(!vdom){
            vdom = newVnode;
            patch(container,vdom);
        }else{
            patch(vdom,newVnode);
        }
    }
    document.getElementById('btn-change').addEventListener('click',function () {
        dataSource[0].name="胡军网";
        dataSource[1].address='南山区沙河东路1号'
        //re——render
        render(dataSource)
    })
    render(dataSource)
</script>
</body>
</html>

  执行结果如下所示:

patch函数——patch(container,vDom)过程的简单实现

/**
 *
 * @param container 容器
 * @param vDom 虚拟dom
 * @constructor
 */

var ulE = {
    tagName: 'ul', // 标签名
    props: { // 属性用对象存储键值对
        id: 'ul-list'
    },
    children: [ // 子节点
        {tagName: 'li', props: {className: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {className: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {className: 'item'}, children: ["Item 3"]},
    ]
}

export default function VDomCreateElement(vDom){
    var tagName=vDom.tagName || '';
    var props =vDom.props || {};
    var children =vDom.children || [];
    var tagNameEle =document.createElement(tagName);
    for(var prop in props){
        if(props.hasOwnProperty(prop)){
            tagNameEle.setAttribute(prop,props[prop])
        }
    }
    if(!children){
        return tagNameEle;
    }else{
        children.forEach(function(item){
            tagNameEle.appendChild(VDomCreateElement(item)) //不断递归生成child Node
        })
    }
    return tagNameEle;
}

patch函数——patch(vDom,newVDom)过程的简单模拟实现

  

/**
 * vDOM 简单diff 对比 新的dom渲染到旧的dom
 * @param vDom 老vDom
 * @param newVDom 新vDom
 */
export function vDomDiff(vDom,newVDom){
    var vDomChilden = vDom.children || [];
    var newVDomChilden = newVDom.children || [];
    //假设 tagName 相同
    vDomChilden.forEach(function(item,index){
        if(!newVDomChilden[index]){
            return;
        }
        if(item.tagName === newVDomChilden[index].tagName){
            //两者tagName 一样 递归
            VDomCreateElement(item,newVDomChilden[index]);
        }else {
            //两者tagName 不一样 替换
            replaceNode(item,newVDomChilden[index])
        }
    })
}

/**
 * dom操作 替换
 * @param vDom
 * @param newVDom
 */
function replaceNode(vDom,newVDom){
    //dom操作 node替换
    // ....

}

  

Visual DOM 为何使用diff算法

 Visual DOM找出DOM 中不同,进而更新DOM,diff算法同样也是找出文件中的不同进行对比,diff应用在linux,git……,

源码地址

https://github.com/10086XIAOZHANG/VirtualDOMDemo  

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:页面内跳转到相应位置的3种方法

下一篇:闲谈Hybrid