深刻浅出理解virtual Dom

 什么是virtual Dom?

Virtual Dom 是虚拟DOM,咱们用JS来模拟DOM结构,结构相似下面的代码:javascript

{
    tag:'ul',
    attrs:{
        id:'list'
    }
    children:[
    {
        tag:'li',
        attrs:{className:'item'},
        children:['item 1']
    },
    {        tag:'li',
        attrs:{className:'item'},
        children:['item 2']
    }
  ]
}复制代码

以上代码模拟的就是这样的DOM结构css

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

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


为何会有Virtual DOM?

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

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

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

若是没有VDOM,咱们会用这样的代码来完成需求jquery

<!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来实现局部渲染。一块儿来感觉一下bash

<!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算法

相关文章
相关标签/搜索