vue源码解读--Vue中的DOM-Diff

804 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

参考自Vue源码系列-Vue中文社区

前言

Vue的视图更新是通过新旧虚拟DOM对比差异,找出需要更新的地方,操作最少真实DOM来实现更新视图。对比 VNode 并找出差异的过程被称为 DOM-Diff 过程。

patch

Vue中, DOM-Diff 过程也叫做 patch 过程。 patch 是“补丁”的意思,以数据发生变化后生成新的 VNode 为标准,对比之前旧的 VNode,如果新的 VNode 上有的节点而旧的 VNode 上没有,就在旧的 VNode 上加上,反之亦然,从而达到来两个 VNode相同。

其实整个过程主要就做了三件事:

  • 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
  • 删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
  • 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode

创建节点

VNode中可以描述6种类型的节点,但只有元素节点、文本节点、注释节点可以被创建插入到 DOM 中。所以在创建节点的时候会先判断是什么节点类型,在调用对应的方法创建并插入到 DOM 中。

// 源码位置: /src/core/vdom/patch.js
function createElm(vnode, parentElm, refElm) {
  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  if (isDef(tag)) {
    vnode.elm = nodeOps.createElement(tag, vnode); // 创建元素节点
    createChildren(vnode, children, insertedVnodeQueue); // 创建元素节点的子节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text); // 创建注释节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text); // 创建文本节点
    insert(parentElm, vnode.elm, refElm); // 插入到DOM中
  }
}
  1. 判断该 VNode 节点是否有 tag 属性,如果有就认为是元素节点,调用 createElement 方法创建元素节点,如果元素节点还有子节点,那就递归遍历创建所有子节点,把所有子节点 insert 到当前元素节点后,再把当前元素节点 insertDOM 中。
  2. 判断 VNodeisComment 属性,如果为 true 则是注释节点,调用 createComment 方法创建注释节点,再插入到 DOM 中。
  3. 如果两个都不满足,则认为是文本节点,调用 createTextNode 方法创建文本节点,再插入到 DOM 中。

2.02d5c7b1.png

删除节点

新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。

function removeNode(el) {
  const parent = nodeOps.parentNode(el); // 获取父节点
  if (isDef(parent)) {
    nodeOps.removeChild(parent, el); // 调用父节点的removeChild方法
  }
}

更新节点

在更新节点中主要分为三种情况判断并分别处理

静态节点

<p>我是不会变化的文字</p>

在这个节点中只包含了纯文字,没有变量,任何数据发生变化都和它无关,称这种节点为静态节点。

如果 VNodeoldVNOde 均为静态节点,则直接跳过,无需处理。

文本节点

如果 VNode 是文本节点,只需看 oldVNode 是否也是文本节点,如果是,则比较内容是否相同,不同则把 oldVNode 里的文本改成跟 VNode 的一样。如果 oldVNode不是文本节点,直接调用 setTextNode方法把它改成文本节点,并且内容和 VNode 相同。

元素节点

如果是元素节点,又被细分为两个情况:

  • 该节点包含子节点

    如果新的节点内包含子节点,如果旧的节点也包含子节点,就递归对比更新子节点;如果旧的节点不包含子节点,那么可能是空节点或者文本节点,如果是空节点,则把新的节点里的子节点创建一份并插入到旧的节点里面,如果是文本节点,则把文本清空,再执行空节点的操作。

  • 该节点不包含子节点

    如果该节点不包含子节点,同时它又不是文本节点,则说明它是个空节点,直接把旧的节点清空即可。

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // vnode与oldVnode是否完全一样?若是,退出程序
  if (oldVnode === vnode) {
    return;
  }
  const elm = (vnode.elm = oldVnode.elm);
​
  // vnode与oldVnode是否都是静态节点?若是,退出程序
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    return;
  }
​
  const oldCh = oldVnode.children;
  const ch = vnode.children;
  // vnode有text属性?若没有:
  if (isUndef(vnode.text)) {
    // vnode的子节点与oldVnode的子节点是否都存在?
    if (isDef(oldCh) && isDef(ch)) {
      // 若都存在,判断子节点是否相同,不同则更新子节点
      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    }
    // 若只有vnode的子节点存在
    else if (isDef(ch)) {
      /**
       * 判断oldVnode是否有文本?
       * 若没有,则把vnode的子节点添加到真实DOM中
       * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
       */
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    }
    // 若只有oldnode的子节点存在
    else if (isDef(oldCh)) {
      // 清空DOM中的子节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1);
    }
    // 若vnode和oldnode都没有子节点,但是oldnode中有文本
    else if (isDef(oldVnode.text)) {
      // 清空oldnode文本
      nodeOps.setTextContent(elm, "");
    }
    // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
  }
  // 若有,vnode的text属性与oldVnode的text属性是否相同?
  else if (oldVnode.text !== vnode.text) {
    // 若不相同:则用vnode的text替换真实DOM的文本
    nodeOps.setTextContent(elm, vnode.text);
  }
}

3.7b0442aa.png

小结

  1. 分析 patch 过程,理解算法思想,知道整个过程就是做三件事:创建节点、删除节点、更新节点。
  2. 针对每个过程对照源码理解其逻辑,看懂流程图。
  3. 如果新旧节点都有子节点,那就需要更新子节点,那么子节点是怎么更新的呢?