我终于理清了vue的diff算法

1、最近好几个月都比较忙,刚好项目接近尾声,今天终于抽出来了一点时间重新捋了一下diff算法,具体的解析后面抽时间补上,画了个大概的图,可以配合代码注释凑活着看
2、草稿箱里也放了好多篇想写的文章,后面整理成一个系列慢慢发出来

1、怎么更好的阅读源码

这个问题困扰我挺久的,在网上搜过大佬们谈怎么阅读源码的心得,总觉得拿来自己套用起来并不适用,这里分享下自己阅读源码时的思路

  • 明确自己的主线任务,即自己读这个源码是想干嘛,写代码过程中遇到问题,需要解惑?还是想了解这个技术的实现原理,学习作者的设计思路。
  • 如果是需要解惑,可以对心里的疑惑先做个推测,大胆的猜一下问题点,然后带着问题去读。
  • 如果是单纯的想要学习,提升自己,推荐先去扒几篇高质量的博客(高赞文章),大致了解下这个技术出现的背景,解决了什么,有什么闪光点,然后可以稍微记下博客中高频出现的关键字,阅读源码的过程可以重点关注下这些闪光点,关键字。
  • 不管出于哪种目的,在分析源码的过程中遇到看不懂的代码块,可以直接暂时跳过,没必要死磕,看不懂可太正常了。如果能感觉到它跟主线任务不搭边,那就直接跳。
  • 读完第一遍之后,心里基本对逻辑有个大概了解了,推荐用画图软件,从头回忆一下刚刚的代码逻辑,画一个思维导图出来,这样可以帮自己理清思路,把知识点串起来,遇到模糊不清的点,也可以倒回去针对性的研究。
  • 源码中经常会调用一些其他文件中的方法,这时候可以根据方法命名去猜一下方法的作用,优秀的源码,作者的命名通常都会很规范。但有些重要的逻辑,可能还是需要跳转到方法的实现大致分析一下。

我读 diff 的原因更多的在于好奇,另外之前在掘金看 v-for为什么不推荐使用index作为key 这篇文章时,看的有点云里雾里 ,懂了但又没懂。另外这些涉及到diff 的文章,都会多次提及 dom复用

2、虚拟dom (virtual dom)

了解过的同学可以直接跳过这一段

前端现在最火的三大框架angular、vue、react都做到了响应式,即数据更新之后,视图会响应数据变更从而重新渲染页面。关于响应式原理这里不做赘述,那么视图重新渲染这一步骤,三大框架是怎么做的呢?

vue 和 react 都使用了 虚拟dom ,配合 diff算法 进行 dom 节点的移动、添加、修改、删除。
而 angular 我扒了很久都没找到对应的文章,但是ng里好像并没有虚拟dom的概念,后面研究后再补上。

所谓虚拟dom就是 用js代码描述一段html代码,简单举个栗子

<li key="li">
	Virtual dom
<li>

转换为虚拟dom

{
   tag: 'li',
   text: 'Virtual dom',
   key: 'li',
   ...
}

vue 通过 createElement 方法创建 虚拟dom,该方法具体做了什么有兴趣的可以自己去github研究下,代码路径如下图
在这里插入图片描述
下面是vue对于虚拟dom的定义,可以简单看一下

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

关于virtual dom的优缺点可以参考这篇 掘金文章

3、patchVnode 更新dom节点

patchVnode的调用入口,在patch函数中

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 如果新的虚拟dom节点不存在,说明旧节点已被删除,直接销毁旧dom节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 如果旧节点不存在,说明是新增,直接创建新的dom
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patchVnode由此处调用
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 安装到真实元素,检查是否是服务器渲染的内容以及是否可以执行
          ... 此处省略一部分逻辑处理,与主线无关
        }

        // 直接创建一个新的dom元素,替换旧的dom
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

这里要注意一点,符合sameVnode条件才会执行patchVnode
在这里插入图片描述
不符合条件的,会直接创建新的dom元素去替换掉旧的,注意看上面的代码走向。

下面是patchVnode的具体实现

/**
 *
 * @param oldVnode 旧的虚拟dom节点
 * @param vnode 新的虚拟dom节点
 * @param insertedVnodeQueue 节点更新队列(队列中的操作都是异步执行,执行时机一般为当前执行周期的最后)
 * @param ownerArray 在patch中调用时为null, updateChildren中为新节点的children
 * @param index 在patch中调用时为null
 * @param removeOnly removeOnly 是一个只有 <transition-group> 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
 * @returns
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  if (oldVnode === vnode) {
    return;
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // 克隆重用的vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

  const elm = (vnode.elm = oldVnode.elm);

  // 看不懂
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }
    return;
  }

  // 为静态树重用元素,仅在克隆 vnode 时执行此操作 -
  // 如果新节点没有被克隆,则意味着渲染函数已经由 hot-reload-api 重置,我们需要进行适当的重新渲染。
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }

  // 看不懂
  let i;
  const data = vnode.data;
  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode);
  }

  const oldCh = oldVnode.children;
  const ch = vnode.children;

  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);
  }
  // 如果新节点中不存在文本
  if (isUndef(vnode.text)) {
    // 如果新旧节点都存在子节点
    if (isDef(oldCh) && isDef(ch)) {
      // 且新旧子节点并不指向同一个,则执行updateChildren
      if (oldCh !== ch)
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    } else if (isDef(ch)) {
      // 如果新节点存在子节点
      if (process.env.NODE_ENV !== 'production') {
        // 如果当前不是生产环境,就检查子节点的key是否重复
        checkDuplicateKeys(ch);
      }
      // 如果旧节点内存在文本,就先把文本设为空
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
      // 然后直接把新节点的子节点添加到elm中
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 如果旧节点存在子节点,而新节点不存在,那就直接删除子节点
      removeVnodes(oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 如果旧节点里存在文本,而新节点不存在,那就直接把文本设为空
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // 如果存在文本且新旧节点内部的文本不相等,就用新节点中的文本覆盖
    nodeOps.setTextContent(elm, vnode.text);
  }
  // 看不懂
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
  }
}

首先需要明确传入的参数都代表什么:

  • oldVnode 为当前dom元素映射成的虚拟dom节点
  • vnode 为数据更新后,通过更新后的数据生成的虚拟dom节点
  • insertedVnodeQueue 为节点插入队列,节点的插入操作都放在该队列中,而这个队列真正使用的地方,是在patch函数的最后,调用了invokeInserthook,消费这个队列内所有的操作,至于invokeInserthook函数的作用,大胆的猜测一下,就是最终执行dom节点的插入操作呗。
/**
 *
 * @param oldVnode 旧的虚拟dom节点
 * @param vnode 新的虚拟dom节点
 * @param insertedVnodeQueue 节点插入队列
 * @param ownerArray 在patch中调用时为null, updateChildren时为新节点的children
 * @param index 在patch中调用时为null
 * @param removeOnly removeOnly 是一个只有 <transition-group> 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
 * @returns
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {

下面来看具体的代码逻辑:

1、先比较新旧节点是否指向同一个引用,如果是相同的,也就是说不需要更新,直接return,本次比较结束

if (oldVnode === vnode) {
    return;
}

2、后面这一大坨我表示基本看不懂,但是跟我的主线任务并不相关,直接跳过
在这里插入图片描述
3、后面这段逻辑是patchVnode方法的核心点

isUndef 判断当前传入的参数是不是 undefined,是则返回true
isDef 判断当前传入的参数是不是undefined,不是则返回true

在这里插入图片描述
ps: 这里本来想用文字写出各种判断的,但是写完发现,还没有思维导图来的清晰
在这里插入图片描述

4、updateChildren

在这里插入图片描述
在patchVnode的逻辑中,当新旧节点都存在子节点,且子节点不同时,会调用updateChildren,进行子节点的更新。

function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  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];
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly;

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh);
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
      // 比较新旧首节点,如果类似就把进行patchVnode操作(加入到节点更新队列)
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 更新节点
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      // 收束指针
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
      // 比较新旧尾结点
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
      //  比较旧首节点和新尾结点
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      // 如果允许移动节点,再更新完节点后,移动该节点到旧尾节点后
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        );
      // 收束指针
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
      // 比较旧尾结点和新首节点
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 如果还没有构建map
      if (isUndef(oldKeyToIdx))
        // 以旧节点的key为key,指针为value构建map
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      // 如果新节点存在key,就在这个map中匹配新节点的key,idxInOld为匹配到的旧节点的索引
      // 如果不存在就循环所有的旧节点,比较新节点是否存在与旧节点中的某个节点相似,匹配上就返回该旧节点的索引
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      // 如果匹配不到就直接创建新节点,插入到旧首节点之前
      if (isUndef(idxInOld)) {
        // New element
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        );
        // key相同或找到类似的节点
      } else {
        vnodeToMove = oldCh[idxInOld];
        // 如果是相似节点,就更新该节点,并且在旧节点中删除该节点,把更新后的节点移动旧首节点前
        // 这里多判断了一遍,过滤掉key相同而元素不同的节点
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // 否则即使是相同的key,但是由于元素不同,依旧会创建新节点
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          );
        }
      }
      // 收束指针
      newStartVnode = newCh[++newStartIdx];
    }
  }
  // 如果因为旧的开始索引大于旧结束索引而结束的循环,则说明还存在新的节点没有对比,直接把这些节点依次加上
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    );
    // 如果是由于新的开始节点大于新结束节点而结束的,则说明新节点相对于旧的节点,移除了一部分,直接删除这部分节点
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}

参数

/**
 * @param {*} parentElm 父元素 (父元素真实的elm)
 * @param {*} oldCh 旧的子节点数组
 * @param {*} newCh 新的子节点数组
 * @param {*} insertedVnodeQueue 队列,同patchVnode
 * @param {*} removeOnly 同patchVnode
 */
function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) 

具体的代码逻辑:

1、声明变量
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];

至于下面这段,后面具体代码逻辑中,用到的时候再解释

 let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

1.1 某支线逻辑

还记得当我们使用v-for时,不小心使用了重复的key,控制台中抛出的错吗?
在这里插入图片描述
2、比较并更新子节点
在了解这部分代码之前,先介绍下sameVnode方法
sameVnode 用于比较节点是否是相似节点,是相似节点,才会进行patchVnode操作,给节点打更新补丁。
该方法主要比较 tag 和 key 是否一致,有兴趣的可以去看下源码

在while循环中,xxxStartIdx 和 xxxEndIdx 都会不停的移动,xxxStartIdx 从 0 向 数组尾部移动,而 xxxEndIdx 从数组尾部,向头部移动。对应的xxxStartVnode 和 xxxEndVnode也会随着idx变化而变化


 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 如果旧的起始节点不存在,这里我也不是很懂,为什么会存在旧节点数组会存在节点为空的情况,不知道在生成子虚拟节点数组时,做了什么处理
    if (isUndef(oldStartVnode)) {
      // 如果不存在则移动指针,更新 oldStartVnode,此时oldStartVnode变成了数组的第二个元素,oldStartIdx也变成了 1
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
      // 同理判断旧结束节点是否存在,不存在则移动旧尾节点指针,更新旧尾结点
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx];
      // 比较新旧首节点,如果类似就把进行patchVnode操作(加入到节点更新队列)
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 更新节点
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      // 收束指针
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
      // 比较新旧尾结点
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
      //  比较旧首节点和新尾结点
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      // 如果允许移动节点,再更新完节点后,移动该节点到旧尾节点后
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        );
      // 收束指针
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
      // 比较旧尾结点和新首节点
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 如果还没有构建map
      if (isUndef(oldKeyToIdx))
        // 以旧节点的key为key,指针为value构建map,此时就用到了上面未解释的那几个变量
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      // 如果新节点存在key,就在这个map中匹配新节点的key,idxInOld为匹配到的旧节点的索引
      // 如果不存在就循环所有的旧节点,比较新节点是否存在与旧节点中的某个节点相似,匹配上就返回该旧节点的索引
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      // 如果匹配不到就直接创建新节点,插入到旧首节点之前
      if (isUndef(idxInOld)) {
        // New element
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm,
          false,
          newCh,
          newStartIdx
        );
        // key相同或找到类似的节点
      } else {
        vnodeToMove = oldCh[idxInOld];
        // 如果是相似节点,就更新该节点,并且在旧节点中删除该节点,把更新后的节点移动旧首节点前
        // 这里多判断了一遍,过滤掉key相同而元素不同的节点
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // 否则即使是相同的key,但是由于元素不同,依旧会创建新节点
          // same key but different element. treat as new element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          );
        }
      }
      // 收束指针
      newStartVnode = newCh[++newStartIdx];
    }
  }

在这里插入图片描述

这部分比较有意思的是比较的设计思路

  1. 先比较新旧节点的首节点是否一致,再比较尾节点是否一致,如果一致,直接进行patchVnode操作
  2. 再比较旧首和新尾,旧尾和新首,判断节点是否直接调换了首尾位置,如果调换了位置,则在更新节点后把节点到对应正确的位置

上述的四步比较用于检查首、尾节点是否发生了偏移。
当上面的四种情况都匹配不上的时候,检查旧节点数组中有没有与该节点构成sameVnhode关系的,有就更新该节点,并且在旧节点中删除该节点,把更新后的节点移动到旧首节点。

查找方式如下:

  1. 以旧节点的key为key,索引(index)为value构建一个对象
    举例:
    旧节点 A,B,C 假如节点的key依次为A,B,C
    生成的对象为 {A:0, B:1, C:2}

  2. 如果新节点存在key,就在上面构成的对象中匹配新节点的key,匹配到就返回value(即匹配到的旧节点在旧节点数组中对应的索引)

  3. 如果新节点不存在key,就循环所有的旧节点,比较新节点是否存在与旧节点中的某个节点相似,匹配上就返回该旧节点的索引

这步判断补上了剩余的所有发生偏移的可能性
在理解这部分逻辑的时候,要明确新旧首尾节点,是随着比较不停变化的,首节点不断向后移动,尾结点不断向前移动,指针Idx在不断向内收缩,直到不符合while条件循环结束。

3、当循环结束时,处理剩余未对比的节点
在这里插入图片描述
从while的条件可以看出,循环结束时有两种情况,要么是旧子节点先循环完,要么新子节点先循环完。

// 如果因为旧的开始索引大于旧结束索引而结束的循环,则说明还存在新的节点没有对比,直接把这些节点依次加上
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(
      parentElm,
      refElm,
      newCh,
      newStartIdx,
      newEndIdx,
      insertedVnodeQueue
    );
    // 如果是由于新的开始节点大于新结束节点而结束的,则说明新节点相对于旧的节点,移除了一部分,直接删除这部分节点
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }

在这里插入图片描述

最后的最后,我们从patchVnode开始,来一起理一下更新过程:

  1. 首先我们更新了数据,触发了vue的视图更新机制,用新的数据生成了新的虚拟dom,然后获取旧数据对应的虚拟dom,调用patch进行对比,patch的逻辑内,相似的节点,调用patchVnode进行更新。
  2. 然后在patchVnode里
    2.1 先判断新旧节点是否存在文本信息,存在则对比文本是否相等,不相等使用新节点的文本覆盖旧节点,相等则不做任何操作。
    2.2 不存在文本时,分支逻辑较多。
    ① 新旧节点存在子节点,旧不存在时执行以下步骤 :step1 如果不是生产环境,就检测key是否重复,重复则在控制台中抛错。step2 旧节点如果存在文本则先把文本置空 step3 把新节点的子节点添加页面元素中。
    ② 旧节点中存在而新节点不存在时,直接删除该页面元素中的子节点。
    ③ 旧节点中存在文本,则先把文本置空
  3. 在updateChildren中,也进行了一连串的逻辑判断,判断子节点的区别,并对不同的逻辑分支做不同的处理。其中部分情况下,比如节点符合sameVnode,会调用patchVnode方法,更新节点信息(子节点的子节点也有可能会触发updateChildren,然后也有可能再触发updateChildren),所以比较会一直进行到节点的最深层。

5、思维导图和整体的代码注释

思维导图地址:https://www.processon.com/view/link/6169523fe0b34d7c7db4a697在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值