美文网首页Vue.jsReact.jsVue.js专区
初步理解Virtual DOM和diff算法

初步理解Virtual DOM和diff算法

作者: JokerPeng | 来源:发表于2018-06-25 01:36 被阅读34次

    一、virtual dom是什么?它为什么会出现?

    1、是什么?
    • virtual dom即 虚拟dom
    • 用js模拟DOM结构
    • DOM变化的对比,放在JS层来做
    • 提高重绘性能
    // 真实的HTML DOM结构
    <ul id='list'>
        <li class='item'>Item 1</li>
        <li class='item'>Item 2</li>
    </ul>
    
    // 用JS模拟这个DOM
    {
        tag: 'ul',
        attrs: {
            id: 'list'
        },
        children: [
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 1']
            }, 
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 2']
            }
        ]
    }
    
    

    DOM操作是非常‘昂贵’的,看似更复杂JS的virtual dom实则效率更高

    // 用jquery实现修改DOM
    <div id="container"></div>
    <button id="change-btn">CHANGE</button>
    <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
    <script>
        const data = [
            {
                name: '彭一',
                age: '18',
                height: '180cm',
                gender: '男',
            },
            {
                name: '彭二',
                age: '19',
                height: '185cm',
                gender: '男',
            },
            {
                name: '彭三',
                age: '20',
                height: '168cm',
                gender: '女',
            },
        ]
    
        // 渲染函数
        let render = data => {
            const $container = $('#container')
    
            // 清空现有内容
            $container.html('')
    
            // 字符串拼接
            const $table = $('<table>')
            $table.append($('<tr><td>姓名</td><td>年龄</td><td>身高</td><td>性别</td></tr>'))
            data.forEach(element => {
                $table.append($(`<tr><td>${element.name}</td><td>${element.age}</td>
                    <td>${element.height}</td><td>${element.gender}</td></tr>`))
            });
    
            // 渲染到页面
            $container.append($table)
        }
    
        // 修改信息
        $('#change-btn').on('click', () => {
            data[1].age = 30
            data[2].height = '178cm'
            render(data)
        })
    
        // 初始化渲染
        render(data)
    </script>
    

    当点击change按钮后,看似只有彭二的age和彭三的height发生改变,但实则整个table表单又重新渲染了一次

    2、遇到的问题
    • DOM操作是‘昂贵’的,JS运行效率高
    • 尽量减少DOM操作,而不是‘推到重来’
    • 项目越复杂,影响越严重
    • Virtual DOM即可解决这个问题
    3、virtual dom存在的必要
    • 用JS模拟DOM结构,效率更高
    • DOM操作‘昂贵’
    • 将DOM对比操作放在JS层,提高效率

    二、virtual dom如何应用,核心API是什么?

    1、如何用?

    能实现virtual dom的库很多,如: snabbdom

    var container = document.getElementById('container');
    
    var vnode = h('div#container.two.classes', {on: {click: someFn}}, [
      h('span', {style: {fontWeight: 'bold'}}, 'This is bold'),
      ' and this is just normal text',
      h('a', {props: {href: '/foo'}}, 'I\'ll take you places!')
    ]);
    // Patch into empty DOM element – this modifies the DOM as a side effect
    patch(container, vnode);
    
    var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
      h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
      ' and this is still just normal text',
      h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
    ]);
    // Second `patch` invocation
    patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
    

    上面一段是snabbdom给出的一个示例,其中 h方法 是创建一个vnode,即虚拟节点,定义一个div,有一个id名container,两个类名twoclasses,绑定一个click事件someFn方法,后面跟着一个数组,数组中有3个元素:

    • 第一个用h方法返回的,一个span,有font-weight样式,和文本内容This is bold
    • 第二个元素就是一个文本字符串:and this is just normal text
    • 第三个元素是一个a元素,有一个属性href链接,后面是a标签文本

    第一个patch方法是将vnode放入到空的container中

    newVnode是返回一个新的node,然后第二个patch是将前后两个node进行一个对比,找出区别,只更新需要改动的内容,其他不更新的内容不更新,这样做到尽可能少的操作DOM。

    h方法抽离出来如下:

    用h方法去具体实现文章开头的那个简单dom节点

    // 真实的HTML DOM结构
    <ul id='list'>
        <li class='item'>Item 1</li>
        <li class='item'>Item 2</li>
    </ul>
    
    // JS模拟这个DOM
    {
        tag: 'ul',
        attrs: {
            id: 'list'
        },
        children: [
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 1']
            }, 
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 2']
            }
        ]
    }
    
    // 用h方法表示这个dom节点:
    var vnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item 2'),
    ])
    

    用virtual dom写法改写jquery的那个demo:

    <div id="container"></div>
    <button id="change-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>
        const snabbdom = window.snabbdom
        // 定义关键函数 patch
        const patch = snabbdom.init([
            snabbdom_class,
            snabbdom_props,
            snabbdom_style,
            snabbdom_eventlisteners
        ])
        
        // 定义关键函数 h
        const h = snabbdom.h
        
        const data = [
                {
                    name: '彭一',
                    age: '18',
                    height: '180cm',
                    gender: '男',
                },
                {
                    name: '彭二',
                    age: '19',
                    height: '185cm',
                    gender: '男',
                },
                {
                    name: '彭三',
                    age: '20',
                    height: '168cm',
                    gender: '女',
                },
            ]
            
        // 把表头放入data数组的第一项
        data.unshift({
            name: '姓名',
            age: '年龄',
            height: '身高',
            gender: '性别'
        })
        
        const container = document.getElementById('container')
        const changeBtn = document.getElementById('change-btn')
        
        let vnode
        // 渲染函数
        let render = (data) => {
            const newVnode = h('table', {}, data.map((item) => {
                let tds = []
                let i
                for (i in item) {
                    // hasOwnProperty检测某个对象是否拥有某个属性,可以有效避免扩展本地原型而引起的错误
                    if (item.hasOwnProperty(i)) {
                        tds.push(h('td', {}, item[i] + ''))
                    }
                }
                return h('tr', {}, tds)
            }))
            
            if (vnode) {
                // 修改后再次渲染
                patch(vnode, newVnode)
            } else {
                // 初次渲染
                patch(container, newVnode)
            }
            // 存储当前vnode赋给newVnode
            vnode = newVnode
        }
        
        // 初次渲染
        render(data)
        
        changeBtn.addEventListener('click', () => {
            data[1].age = 30
            data[2].height = '178cm'
            
            // 再次渲染data
            render(data)
        })
        </script>
    
    2、核心API
    • h('<标签名>', {属性}, [子元素])
    • h('<标签名>', {属性}, '文本字符串')
    • 初次渲染:patch(container, vnode)
    • 再次修改后DOM渲染:patch(vnode, newVnode)

    三、diff算法

    1、什么是diff算法

    日常开发中都会用到diff,最普通的linux基础命令diff两个文件,找出不同,还有就是git命令比对前后修改内容

    // 两个对象分别放在两个json中
    // data1.json
    {
        "name": "pengxiaohua",
        "age": 18,
        "height": 184
    }
    
    // data2.json
    {
        "name": "xiaohua",
        "age": 18,
        "height": 183
    }
    
    // 控制台输入 diff data1.json data2.json,得出:
    2c2
    <     "name": "pengxiaohua",
    ---
    >     "name": "xiaohua",
    4c4
    <     "height": 184
    ---
    >     "height": 183
    

    同时在git命令中的git diff XXXX 也可以用来比对文件修改前后的差别

    virtual dom为何用diff算法?

    • DOM操作是昂贵的,应该尽可能减少DOM操作
    • 找出本次必须更新的节点,其他的不用更新
    • 这个“找出”的过程,就需要diff算法

    一句话,virtual dom中应用diff算法是为了找出需要更新的节点

    2、diff算法实现流程

    diff的实现过程就是 patch(container, vnode) 和 - patch(vnode, newVnode)

    diff的实现的核心就是 createElementupdateChildren

    • patch(container, vnode)
      初始化加载,直接将 vnode 节点打包渲染到一个空的容器 container

    文章开头可以看到,用JS去模拟一个简单的DOM节点

    // 真实的HTML DOM结构
    <ul id='list'>
        <li class='item'>Item 1</li>
        <li class='item'>Item 2</li>
    </ul>
    
    // JS模拟这个DOM
    {
        tag: 'ul',
        attrs: {
            id: 'list'
        },
        children: [
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 1']
            }, 
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 2']
            }
        ]
    }
    
    

    那么模拟完了之后,怎么将模拟的JS进行转化为真实的DOM的呢?这个转化过程可以用这样一个 createElement 函数来描述:

    function createElement (vnode) {
        var tag = vnode.tag
        var attrs = vnode.attrs || {}
        var children = vnode.children || []
        if(!tag) {
            return null
        }
        
        // 创建真实的 DOM 元素
        var ele = document.createElement(tag)
        // 属性
        var attrName
        for (attrName in attrs) {
            if (attrs.hasOwnProperty(attrName)) {
                elem.setAttribute(attrName, attrs[attrName])
            }
        }
        
        // 子元素
        children.forEach(function (childVnode) {
            // 递归调用 createElement 创建子元素
            elem.appendChild(createElement(childVnode))
        })
        // 返回真实的 DOM 元素
        return elem
    }
    

    当我们修改子节点,如下操作后,就要用到 patch(vnode, newVnode)

    • patch(vnode, newVnode)
      数据改变后,patch对比老数据 vnode 和新数据 newVnode
    // 真实的HTML DOM结构
    <ul id='list'>
        <li class='item'>Item 1</li>
        <li class='item'>Item 22</li>
        <li class='item'>Item 3</li>
    </ul>
    
    // JS模拟这个DOM
    {
        tag: 'ul',
        attrs: {
            id: 'list'
        },
        children: [
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 1']
            },
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 22']
            },
            {
              tag: 'li',
              attrs: {className: 'item'},
              children: ['Item 3']
            }
        ]
    }
    

    这个转化过程,其实就是遍历子节点,然后找出区别,如下面的方法 updateChildren:

    function updateChildren (vnode, newVnode) {
        var children = vnode.children || []
        var newChildren = newVnode.children || []
        
        // 遍历现有的 children
        children.forEach(function (child, index) {
            var newChild = newChildren[index]
            if (newChild == null) {
                return
            }
            if (child.tag === newChildren.tag) {
                // 两者 tag 一样
                updateChildren(child, newChild)
            } else {
                // 两者 tag 不一样
                replaceNode(child, newChild)
            }
        })
    }
    
    function replaceNode (vnode, newVnode) {
        // 真实的DOM节点
        var elem = vnode.elem
        var newElem = createElement(newVnode)
        
        // 替换(此处代码太过复杂,略省无数字)
        ... ...
    }
    
    3、diff算法做了哪些事:
    • 节点的新增和删除
    • 节点重新排序
    • 节点属性、样式、事件绑定
    • 如何极致压榨性能
    • ... ...

    相关文章

      网友评论

        本文标题:初步理解Virtual DOM和diff算法

        本文链接:https://www.haomeiwen.com/subject/wgleyftx.html