虚拟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的方式是有多么费时,dom操作是费时的,
但是Js作为一门语言,运行速度是非常快的,我们如果在Js层做dom对比,尽量减少不必要的dom操作,而不是每一次都全部翻修,我们的效率就会大大增加。
而vdom就可以完美解决这个问题。

 

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

 

diff算法是用来比较虚拟dom差异的算法

v2主要依据首尾指针法。

按新旧头头 尾尾 头尾 尾头去比较,若是都不存在,则去寻找在旧dom是否有与当时新dom的key值一样的数据,若有则移动,无则新增一个

最后满足old子节点的头尾交叉,或new子节点的头尾交叉。说明对比完了。

此时如果old交叉,new未交叉,说明new子节点剩下的都是要新创建并插入。反之,old未交叉,new交叉,说明old子节点剩下的是多余的,需要从dom中移除。

 

v3 diff算法分为有key和无key
无key
1.新虚拟dom对旧虚拟dom进行替换,替换到最后
2.若新dom有剩余则新增
3.反之删除
有key
1.前序对比,新旧虚拟dom头头进行比对,直到不一样
2.后序对比新旧虚拟dom尾尾比对,直到不一样
3.新节点有剩余新增
4.旧节点有剩余移除
5.特殊情况无序(最长递增子序列算法)

posted on 2021-12-16 14:57  sss大辉  阅读(119)  评论(0编辑  收藏  举报

导航