【Vue源码】第十八节DOM更新以及diff算法

_update

之前学过,vue通过_update将虚拟DOM转为真实的DOM并更新视图,但是只介绍了初始化的时候,并没有讲到更新。当我们修改数据的时候,页面也应该会发生更新,同样会走到_update中。_update会判断是首次渲染,还是更新视图。_update主要调用了_patch__方法。

patch

这次主要介绍的是更新时候的__patch__,是这么调用的:

vm.$el = vm.__patch__(prevVnode, vnode)

patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较viturl dom的变更,得出变化以修改视图。

diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fB3rKj2A-1589083627542)(E:\note\vue\vue源码解读笔记\Vue的diff算法.assets\2.png)]

两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化。

在源码中,patch会比较新旧VNode,然后做出不同的处理。

首先学习以下怎么比较两个新旧的VNode,是使用了sameVnode方法。

sameVnode

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

sameVnode 的逻辑非常简单,如果两个 vnodekey 不相等,则是不同的;否则继续判断对于同步组件,则判断 isCommentdatainput 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同。

所以根据新旧 vnode 是否为 sameVnodepatch会走到不同的更新逻辑。

新旧节点不同时

如果sameVnode返回false时,会经历三个步骤:

  • 调用createElm创建新节点;
  • 更新父的占位符节点;
  • 删除旧节点。

新旧节点相同时

sameVnode返回true是,会调用patchVnode方法,这个方法作用就是把新的 vnode patch 到旧的 vnode

prepatch

当更新的 vnode 是一个组件 vnode 的时候,会执行prepatch

let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode)
}

prepatch会拿到新的 vnode 的组件配置以及组件实例,然后调用updateChildComponent函数对占位符 vm.$vnodeslotlistenersprops 等等进行更新

update

if (isDef(data) && isPatchable(vnode)) {
  for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}

在执行完新的 vnodeprepatch 钩子函数,会执行所有 moduleupdate 钩子函数以及用户自定义 update 钩子函数,对于 module 的钩子函数。

完成patch过程

if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  } else if (isDef(ch)) {
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  } else if (isDef(oldCh)) {
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '')
  }
} else if (oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text)
}

这个环节有两大类:

  1. vnode是文本类型,且新旧节点的text内容不相等时,则直接替换文本节点:

     else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    
  2. vnode不是文本类型时,则判断它们的子节点:

    • oldChch 都存在且不相同时,使用 updateChildren 函数来更新子节点(updateChildren划重点);

    • 如果只有ch存在表示旧节点不需要了,如果旧的节点是文本节点则先将节点的文本清除,然后通过 addVnodesch 批量插入到新节点 elm 下;

    • 如果只有oldCh,则直接移除;

    • 如果旧节点是文本节点,清除其节点文本内容。

执行 postpatch 钩子函数

if (isDef(data)) {
  if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}

执行完 patch 过程后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有则执行。

这样就完成了DOM的更新了。

我们知道Vue是先渲染VNode,再将VNode转为真实的DOM,所以每次更新时,都会先更新VNode先,所以会执行patch方法,我们再上面已经大致分析了patch的过程,但是还没有具体分析,更新是,新旧节点都有子内容时会调用的updateChildren方法,这个方法就是diff的核心。

updateChildren

一开始的时候,会出现这么几行代码,你可以理解成你现在要更新视图,你的新旧节点下面有多个子节点:

let oldStartIdx = 0
let 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]
  • oldStartIdx指向的是old节点的第一个节点的下标;
  • newStartIdx指向的是new节点的第一个节点的下标;
  • oldEndIdx指向的是old节点的第一个节点的下标;
  • oldStartVnode指向的是old节点的第一个节点;
  • oldEndVnode指向的是old节点的最后一个节点;
  • newEndIdx指向的是new节点的第一个节点的下标;
  • newStartVnode指向的是new节点的第一个节点;
  • newEndVnode指向的是new节点的最后一个节点;

举例,要完成这样的视图更新,从ABCD变为DCBAE:
在这里插入图片描述

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // ...
}

我们会对新旧子节点进行遍历,当满足上述条件才会跳出循环。接下来分析每一种情况:

  • oldStartVnodeundefine的情况下,就让oldStartVnode等于下一个兄弟节点

    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } 
    
  • oldEndVnodeundefine的情况下,就让oldEndVnode等于上一个兄弟节点

    else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } 
    
  • oldStartVnodenewStartVnodesameVnode时,会继续调用patchVnode,然后将oldStartVnodenewStartVnode移动到下一个兄弟节点

    else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    

在这里插入图片描述

  • oldEndVnodenewEndVnodesameVnode时,会继续调用patchVnode,然后将oldEndVnodenewEndVnode移动到上一个兄弟节点。

    else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } 
    
  • oldStartVnodenewEndVnodesameVnode时,这说明oldStartVnode跑到了newEndVnode的后面,进行patchVnode的同时还需要将真实DOM节点移动到newEndVnode的后面。

    else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } 
    

在这里插入图片描述

  • oldStartVnodenewEndVnodesameVnode时,这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。

    else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
          patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
          canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
          oldEndVnode = oldCh[--oldEndIdx]
          newStartVnode = newCh[++newStartIdx]
        } 
    

在这里插入图片描述

  • 如果以上情况均不符合,

    (1) 则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNodevalue为对应index序列的哈希表。

    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    

    (2)从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,

    idxInOld = isDef(newStartVnode.key)
            ? oldKeyToIdx[newStartVnode.key]
            : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    

    如果找到且同时满足sameVnodepatchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。(比如说newStartIdx对应的keyoldCh中某个key相等,那么就会把这个oldCh某个key相等的节点,移动到oldChildnewStartIdx对应的位置去)

在这里插入图片描述
当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。

在这里插入图片描述

循环结束后,如果oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。

if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } 

在这里插入图片描述

同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。

else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }

在这里插入图片描述

以上步骤只是将虚拟DOM映射成了真实的DOM。最后会依赖于虚拟DOM的生命钩子,给这些DOM加入attrclassstyle等DOM属性。

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

总结

组件更新的过程核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。

新旧节点不同的更新流程是:创建新节点->更新父占位符节点->删除旧节点;

而新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。最复杂的情况是新旧节点相同且它们都存在子节点,那么会执行 updateChildren 逻辑。

最后我们还需要调用VNode的生命钩子给DOM入attrclassstyle等DOM属性。

参考链接

VirtualDOM与diff(Vue实现)
Vue.js 技术揭秘

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值