了解Vue的虚拟DOM和diff算法

前情提要:

相信对于使用或react的人来说很熟悉,而他们常常也扮演着重要的角色,但对于我这种很少使用react,使用vue多一些的来说只能根据vue来展开介绍,有失误地方请无情的鞭策我谢谢

一、diff算法

  • diff算法主要描述了当数据发生改变时dom都经历了什么
  • 也就是说在数据变化的时候,vue是如何来改变视图的呢?其实so easy,首先一开始会根据真实DOM生成一个虚拟DOM,当虚拟DOM某个节点的数据发生改变后会生成一个新的Vnode,然后VNode和oldVnode对比,把不同的地方修改在真实DOM上,最后再使得oldVnode的值为Vnode。
  • Vnode在我自己理解就是JavaScript对象然后VNode表示Virtual DOM,也就是虚拟 DOM。是Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪。

diff过程就是通过调用patch函数,来比较新旧节点,一边比较一边给真实DOM打补丁(patch)

首先先来了解一下pach
function patch (oldVnode, vnode) {
 // some code
 if (sameVnode(oldVnode, vnode)) {
  patchVnode(oldVnode, vnode)
 } else {
  const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
  let parentEle = api.parentNode(oEl) // 父元素
  createEle(vnode) // 根据Vnode生成新元素
  if (parentEle !== null) {
   api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
   api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
   oldVnode = null
  }
 }
 // some code 
 return vnode
}

这里patch函数接收两个参数 oldVnode 和 Vnode 分别代表新的节点和之前的旧节点,然后判断两节点是否值得比较,值得比较则执行 patchVnode

function sameVnode (a, b) {
 return (
 a.key === b.key && // key值
 a.tag === b.tag && // 标签名
 a.isComment === b.isComment && // 是否为注释节点
 // 是否都定义了data,data包含一些具体信息,例如onclick , style
 isDef(a.data) === isDef(b.data) && 
 sameInputType(a, b) // 当标签是<input>的时候,type必须相同
 )
}

不值得比较则用 Vnode 替换 oldVnode
如果两个节点都是一样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明 Vnode 完全被改变了,就可以直接替换 oldVnode 。

虽然这两个节点不一样但是他们的子节点一样怎么办?其实diff可是逐层比较的,如果第一层不一样那么就不会继续深入比较第二层了。

patchVnode

既然新节点与老节点都在同一节点,那么这个方法做了什么处理?

function patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el           //找到对应的真实DOM
    let i, oldCh = oldVnode.children, ch = vnode.children     
    if (oldVnode === vnode) return         //如果新老节点相同,直接返回
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        //如果新老节点都有文本节点且不相等,那么新节点的文本节点替换老节点的文本节点
        api.setTextContent(el, vnode.text)  
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            //如果新老节点都有子节点,执行updateChildren比较子节点[很重要也很复杂,下面展开介绍]
            updateChildren(el, oldCh, ch)
        }else if (ch){
            //如果新节点有子节点而老节点没有子节点,那么将新节点的子节点添加到老节点上
            createEle(vnode)
        }else if (oldCh){
            //如果新节点没有子节点而老节点有子节点,那么删除老节点的子节点
            api.removeChildren(el)
        }
    }
}

假如两个节点不一样,那么新的节点就会代替老的节点
如果两个节点一样:

  1. 新老节点一样,直接返回;
  2. 老节点有子节点,新节点没有:删除老节点的子节点;
  3.老节点没有子节点,新节点有子节点:新节点的子节点直接append到老节点;
  4.都只有文本节点:直接用新节点的文本节点替换老的文本节点;
  5.都有子节点:updateChildren
updateChildren
updateChildren (parentElm, oldCh, newCh) {
 let oldStartIdx = 0, newStartIdx = 0
 let oldEndIdx = oldCh.length - 1
 let oldStartVnode = oldCh[0]
 let oldEndVnode = oldCh[oldEndIdx]
 let newEndIdx = newCh.length - 1
 let newStartVnode = newCh[0]
 let newEndVnode = newCh[newEndIdx]
 let oldKeyToIdx
 let idxInOld
 let elmToMove
 let before
 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
   oldStartVnode = oldCh[++oldStartIdx] 
  }else if (oldEndVnode == null) {
   oldEndVnode = oldCh[--oldEndIdx]
  }else if (newStartVnode == null) {
   newStartVnode = newCh[++newStartIdx]
  }else if (newEndVnode == null) {
   newEndVnode = newCh[--newEndIdx]
  }else if (sameVnode(oldStartVnode, newStartVnode)) {
   patchVnode(oldStartVnode, newStartVnode)
   oldStartVnode = oldCh[++oldStartIdx]
   newStartVnode = newCh[++newStartIdx]
  }else if (sameVnode(oldEndVnode, newEndVnode)) {
   patchVnode(oldEndVnode, newEndVnode)
   oldEndVnode = oldCh[--oldEndIdx]
   newEndVnode = newCh[--newEndIdx]
  }else if (sameVnode(oldStartVnode, newEndVnode)) {
   patchVnode(oldStartVnode, newEndVnode)
   api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
   oldStartVnode = oldCh[++oldStartIdx]
   newEndVnode = newCh[--newEndIdx]
  }else if (sameVnode(oldEndVnode, newStartVnode)) {
   patchVnode(oldEndVnode, newStartVnode)
   api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
   oldEndVnode = oldCh[--oldEndIdx]
   newStartVnode = newCh[++newStartIdx]
  }else {
   // 使用key时的比较
   if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
   }
   idxInOld = oldKeyToIdx[newStartVnode.key]
   if (!idxInOld) {
    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
    newStartVnode = newCh[++newStartIdx]
   }
   else {
    elmToMove = oldCh[idxInOld]
    if (elmToMove.sel !== newStartVnode.sel) {
     api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
    }else {
     patchVnode(elmToMove, newStartVnode)
     oldCh[idxInOld] = null
     api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
    }
    newStartVnode = newCh[++newStartIdx]
   }
  }
 }
 if (oldStartIdx > oldEndIdx) {
  before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
  addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
 }else if (newStartIdx > newEndIdx) {
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
 }
}

代码有点多,但首先先说一下这个函数做了什么

1.将 Vnode 的子节点 Vch 和 oldVnode 的子节点 oldCh 提取出来
2.oldCh 和 vCh 各有两个头尾的变量 StartIdx 和 EndIdx ,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了 key ,就会用 key 进行比较,在比较的过程中,变量会往中间靠,一旦 StartIdx>EndIdx 表明 oldCh 和 vCh 至少有一个已经遍历完了,就会结束比较。

.

第一步:
在这里插入图片描述

oldStartIdx = A , oldEndIdx = C;
newStartIdx = A , newEndIdx = D;

此时oldStartIdx和newStarIdx匹配,所以将dom中的A节点放到第一个位置,此时A已经在第一个位置,所以不做处理,此时真实DOM顺序:A B C

第二步:
在这里插入图片描述

oldStartIdx = B , oldEndIdx = C;
newStartIdx = C , oldEndIdx = D;

此时oldEndIdx和newStartIdx匹配,将原本的C节点移动到A后面,此时真实DOM顺序:A C B;

第三步:

oldStartIdx = C , oldEndIdx = C;
newStartIdx = B , newEndIdx = D;
oldStartIdx++,oldEndIdx–;
oldStartIdx > oldEndIdx

此时遍历结束,oldCh已经遍历完,那么将剩余的ch节点根据自己的index插入到真实DOM中即可,此时真实DOM顺序:A C B D;

  • 所以匹配过程中判断结束有两个条件:
  • oldStartIdx > oldEndIdx表示oldCh先遍历完成,如果ch有剩余节点就根据对应index添加到真实DOM中;
  • newStartIdx > newEndIdx表示ch先遍历完成,那么就要在真实DOM中将多余节点删除掉;

最后,记录diff
在这里插入图片描述

二、虚拟DOM

1、 什么是虚拟DOM?

虚拟 dom 是相对于浏览器所渲染出来的真实 dom 的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新 ui 的目的。

这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树,那么每次 dom 的更改就变成了 js 对象的属性的更改,这样一来就能查找 js 对象的属性变化要比查询 dom 树的性能开销小。

2、为什么使用虚拟DOM?

起初我们在使用JS/JQuery时,不可避免的会大量操作DOM,而DOM的变化又会引发回流或重绘,从而降低页面渲染性能。那么怎样来减少对DOM的操作呢?此时虚拟DOM应用而生,所以虚拟DOM出现的主要目的就是为了减少频繁操作DOM而引起回流重绘所引发的性能问题的!

3、虚拟DOM和真实DOM的区别?
  • 虚拟DOM不会进行回流和重绘;
  • 真实DOM在频繁操作时引发的回流重绘导致性能很低;
  • 虚拟DOM频繁修改,然后一次性对比差异并修改真实DOM,最后进行依次回流重绘,减少了真实DOM中多次回流重绘引起的性能损耗;
  • 虚拟DOM有效降低大面积的重绘与排版,因为是和真实DOM对比,更新差异部分,所以只渲染局部;

总损耗 = 真实DOM增删改 + (多节点)回流/重绘; //计算使用真实DOM的损耗
总损耗 = 虚拟DOM增删改 + (diff对比)真实DOM差异化增删改 + (较少节点)回流/重绘; //计算使用虚拟DOM的损耗

4、虚拟DOM的好处是什么呢?
  • 兼容性好。因为Vnode本质是JS对象,所以不管Node还是浏览器环境,都可以操作;
  • 减少了对Dom的操作。页面中的数据和状态变化,都通过Vnode对比,只需要在比对完之后更新DOM,不需要频繁操作,提高了页面性能;
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值