深入浅出理解virtual Dom

 什么是virtual Dom?

Virtual Dom 是虚拟DOM,我们用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结构

<ul>
    <li class='item'>item 1</li>
    <li class='item'>item 2</li></ul>复制代码

那么为什么会有VDOM(virtual dom简称)这样的结构呢?


为什么会有Virtual DOM?

我们来模拟这样的一个场景需求。

1.有一堆数据,需要将数据渲染成表格

2.随便修改一个信息,表格也会跟着变化

如果没有VDOM,我们会用这样的代码来完成需求

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"> 
   <title>Document</title></head><body>
    <div id="container"></div>

    <button id="btn-change">change</button>
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
    <script type="text/javascript">
        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)    
</script>
</body>
</html>

复制代码

上面的代码虽然完成了需求,但是,遗憾的是,如果我只是修改一部分数据,整个table

都需要全部渲染。对于浏览器而言,渲染DOM是一个非常“昂贵“的过程。那么,有没有什么办法,修改部分数据的时候,只是渲染我修改的DOM呢?


使用VDOM实现只渲染修改的DOM

我们首先来使用一下snabbdom这个库,它会利用VDOM来实现局部渲染。一起来感受一下

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title></head><body>
    <div id="container"></div>
    <button id="btn-change">change</button>

    <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-class.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-props.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-style.js"></script> 
   <script src="https://cdn.bootcss.com/snabbdom/0.7.0/snabbdom-eventlisteners.js"></script>
    <script src="https://cdn.bootcss.com/snabbdom/0.7.0/h.js"></script>
    <script type="text/javascript">
        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) {
                // 如果已经渲染了
                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)
        })
    </script>
</body>
</html>复制代码


以上代码,则实现了当你修改了部分数据时,只渲染一部分数据。

那么,以上代码的核心就是两个函数,我们需要对此来做探讨。一个是函数h,一个是函数patch


关键函数h和关键函数patch

关键函数h

函数h返回的值是一个vnode,也就是虚拟DOM节点,如图所示


也就是说使用h函数可以生成类似于右边的vnode结构。


关键函数patch

那么关键函数patch的作用,则是将vnode渲染成真实的DOM节点,然后塞入到容器里面。

如果容器里面已经有生成好的vnode,那么,则会将新生成的newVnode和之前的vnode相比较,然后将不同的节点找出来,然后代替旧的节点。


到现在为止,已经基本上了解了VDOM的含义以及为什么会用VDOM,我们来做一个简单的总结

  1. 如果只有部分数据变动,却要因此渲染整个DOM
  2. DOM是非常“昂贵”的操作,因此我们需要减少DOM操作
  3. 找出必须要更新的节点,其他的则可以不更新


那么,接下来又出现了一个问题,我们如何知道哪个节点需要更新呢?这就是diff算法的作用


diff算法找出需要更新的DOM

因为diff算法本身太过于复杂,所以只需要理解一下核心的思想即可。

那么我们只需要关注在渲染的时候,发生了什么事情,理解下面这两个事件的核心流程即可。

  1. patch(container, newVnode)
  2. patch(vnode, newVnode)

也就是说,我们需要理解的是:

  1. 初次渲染的时候,将VDOM渲染成真正的DOM然后插入到容器里面。
  2. 再次渲染的时候,将新的vnode和旧的vnode相对比,然后如何进行局部渲染的过程。


1.patch(container, newVnode)

我们要实现的是这样的过程:


我们来模拟一下上面的创建过程,只是伪代码,我们了解大致的流程

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
}复制代码

那么,通过上面的模拟代码,已经可以很好的了解最开始将vdom渲染到容器的过程。


2.patch(vnode, newVnode)

这个过程就是将newVnode和vnode对比,将差异进行渲染的部分。



那么伪代码流程如下:

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)//生成新的真实的dom节点 
   // 替换
}复制代码

那么真正的替换过程有哪些呢?简单的总结一下:

  • 找到对应的真实dom,称为elem
  • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而newVnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而newVnode有,则将Vnode的子节点真实化之后添加到elem
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要,请参考这篇文章


以上只是简单的理解了diff算法的流程,关于更多的diff算法的详细过程,可以阅读参考文章。


参考文章

详解vue的diff算法


转载于:https://juejin.im/post/5d0228436fb9a07f0357354d

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值