虚拟dom与diff算法

1.虚拟dom

dom就是html文件里内容,一个页面由多个dom组成

<ul class="lists">
    <li class="item">li1</li>
    <li class="item">li2</li>
</ul>

而对应的虚拟dom是

tag: 'ul',
attrs: {
    className: 'lists'
},
children: [
    {
        tag: 'li',
        attrs: {
            className: 'item'
        },
        children: ['li1']
    },
    {
        tag: 'li',
        attrs: {
            className: 'item'
        },
        children: ['li2']
    }
]

tag表示标签名,attrs就是dom的属性,每个dom如果有children的话,就会在children中以数组的形式展示,数组的每一项就又是一个虚拟dom结构。

 

为什么要使用虚拟dom呢?

举个最简单的列子

使用jq的时候,使用append插入函数

要是后续改了某个值,要重新append.是整个dom发生的替换,并不是修改的那一项

 

并且单单一个空白的div底下的标签就有那么多

var div = document.createElement('div')
var item,
    result = ''
for (item in div) {
    result += ' | ' + item
}
console.log(result)

 

虚拟dom与diff算法

 

 

 

密密麻麻的属性,更何况这还只是一级属性,可想而知直接操作dom的方式是有多么费时,dom操作是费时的, 但是Js作为一门语言,运行速度是非常快的,我们如果在Js层做dom对比,尽量减少不必要的dom操作,而不是每一次都全部翻修,我们的效率就会大大增加。 而vdom就可以完美解决这个问题。

 

要了解如何使用vdom,我们可以借助现有的vdom实现库,来了解其API,进而了解如何将vdom运用于开发中。

这里我们选择一个Vue2中使用的虚拟dom库 snabbdom

主要有两个函数:

h函数

可以看到 h 函数,有三个参数

  • 标签选择器
  • 属性
  • 子节点

比如说第一个h函数生成的vnode,就是一个ul标签,绑定了className为lists,

第一个children为带有className的li,li里是一个文本节点li1,

第一个children为带有className的li,li里是一个文本节点li2,

 

patch函数

patch 分为两种情况

  • 第一种是第一次渲染的时候 patch将vnode丢到container空容器中
       var vnode = h('ul#list',{},[
        h('li.item',{},'大冰哥'),
        h('li.item',{},'伦哥'),
        h('li.item',{},'阿孔')
      ])
    
      patch(container, vnode) // vnode 将 container 节点替换
    复制代码

第一次patch渲染的时候,是将生成的vnode往空容器里丢 可以对比之前的Jquery第一次渲染表格的时候,将table html append到容器中去

  • 第二种是更新节点的时候,newVnodeoldVnode替换
    btn.addEventListener('click',function() {
      var newVnode = h('ul#list',{},[
        h('li.item',{},'大冰哥'),
        h('li.item',{},'伦哥'),
        h('li.item',{},'孔祥宇'),
        h('li.item',{},'小老弟'),
      ])
      patch(vnode, newVnode)
    })
    复制代码

这里的patch就会将的vonde和之前的vnode进行比对,只修改改动的地方,没动的地方保持不变,这里的核心就是涉及的diff算法

 

虚拟dom与diff算法

 

我们可以清楚的看到,相对于之前的JQuery整个页面dom全部替换的情况,用vdom的pathc函数只修改了我们相对老的vnode变动的地方,没改动的地方就没用动(从页面的闪烁可以看出来)

 

总结

vdom的核心api

  • h('标签名', '属性', [子元素])
  • h('标签名', '属性', '文本')
  • patch(container, vnode)  //初次
  • patch(oldVnode,newVnode) // 修改

 

2.diff算法

什么是diff算法

我们在平时工作中,其实很多时候都会使用到diff算法

比如你在git提交代码的时候使用的 git diff 命令,再或者是网上的一些代码比对工具如svn上的,vue的key后续会说

而我们的虚拟dom,核心就是diff算法,我们前面讲过,找出有必要更新的节点更新,没有更新的节点就不要动。

这其中的核心就是如何找出哪些更新哪些不更新,这个过程就需要diff算法来完成

 

patch(container, vnode)

这个patch的过程是将一个vnode(vdom)添加到空容器生成真实dom的过程,主要的简化代码流程如下:

function creatElement(vnode) {
  let tag = vnode.tag
  let attrs = vnode.attrs || {}
  let children = vnode.children || []
  // 无标签 直接跳出
  if (!tag) {
    return null
  }
  // 创建元素
  let elem = document.createElement(tag)
  // 添加属性
  for(let attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
      elem.setAttribute(arrtName, arrts[attrName])
    }
  }
  // 递归创建子元素
  children.forEach((childVnode) => {
    elem.appendChild(createElement(childVnode))
  })

  return elem
}

 

简化后的代码很简单,大家也都能够理解,其中的一个重要的点就是 自递归调用生成孩子节点,终止条件就是tagnull的情况

 

patch(vnode, newVnode)

这个patch过程就是比较差异的过程,我们这里就只模拟最简单的场景

// 简化流程 假设跟标签相同的两个虚拟dom
function updateChildren (vnode, newVnode) {
  let children = vnode.children || []
  let newChildren = newVnode.children || []

  // 遍历现有的孩子
  children.forEach((oldChild, index) => {
    let newChild = newChildren[index]
    if (newChild === null) {
      return
    }
    // 两者tag一样,值得比较
    if (oldChild.tag === newChild.tag) {
      // 递归继续比较子项
      updateChildren(oldchild, newChild)
    } else {
      // 两者tag不一样
      replaceNode(oldChild, newChild)
    }
  })
}

这里面的点就也递归,这里只是简单的拿tag来判断更新条件,其实实际的比这复杂很多很多;

replace函数实际的操作就是将newVnode新生成的真实dom将老的dom替换掉,这里涉及更多的是原生dom操作,就不在赘述了。

Vue 中的 key 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的唯一 id。 我们在使用的使用经常会使用index(即数组的下标)来作为key,但其实这是不推荐的一种使用方法 要是出现如下情况: 在第二条加了一条数据
之前的数据                         之后的数据

key: 0  index: 0 name: test1     key: 0  index: 0 name: test1
key: 1  index: 1 name: test2     key: 1  index: 1 name: 不甘落后跑到第二的的一条数据
key: 2  index: 2 name: test3     key: 2  index: 2 name: test2
                                 key: 3  index: 3 name: test3。

这样一来,追加数据以后,除了第一条数据能够就地复用,后三条都要重新渲染,这显然不是我们想要的结果。

所以我们需要使用key来给每个节点做一个唯一标识,Vue的Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点,所以一句话,key的作用主要是为了高效的更新虚拟DOM

 

原文链接:https://juejin.cn/post/6844903767473651720#heading-5

上一篇:Android资源


下一篇:可视化常见问题解决方案(九)背景颜色问题