手记

初步理解Virtual DOM和diff算法

一、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 effectpatch(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` invocationpatch(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算法做了哪些事:
  • 节点的新增和删除

  • 节点重新排序

  • 节点属性、样式、事件绑定

  • 如何极致压榨性能

  • ... ...



作者:JokerPeng
链接:https://www.jianshu.com/p/6ca5c22439f5


0人推荐
随时随地看视频
慕课网APP