vue源码学习(八)patch

Vue.js的虚拟DOM通过patch方法实现高效更新。首次渲染时,创建元素并插入到DOM中;当节点不同时,创建新节点或删除旧节点;相同节点则进行更新。创建节点涉及元素、注释和文本节点的创建;更新节点通过比对新旧节点,执行相应操作如文本变更、属性更新、子节点对比;删除节点则从DOM中移除。整个过程中,Vue.js利用key进行高效的节点匹配,提高性能。
摘要由CSDN通过智能技术生成

vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js

虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM。通过比对新旧两个vnode,找出需要更新的节点进行更新。

更新的操作包括:

  • 创建新增的节点
  • 删除已经废弃的节点
  • 修改需要更新的节点

新增节点

什么情况下创建元素并将元素渲染到视图?

 1. 首次渲染时,DOM中不存在任何节点,oldVnode并不存在,使用vnode创建元素并渲染视图;

如模板:

<div id="app">
  <h1>{{message}}</h1>
</div>

首次渲染时,先创建元素并插入body里,并移除之前的模板

2. 当oldVnode与vnode完全不是同一个节点时,是使用vnode创建一个新DOM节点,用它去替换oldVnode所对应的真实DOM节点

创建节点

只有三种类型的节点会被创建并插入到DOM中:元素节点、注释节点、文本节点。

元素节点

    判断vnode是否是元素节点,只需要判断它是否具有tag属性即可;是元素节点就调用当前环境下的createElement方法(在浏览器环境下就是document.createElement)来创建真实的元素节点。

    将元素渲染到视图只需要调用当前环境下的appendChild方法(在浏览器环境下就是document.appendChild),就可以将一个元素插入到指定的父节点中。

    元素节点如果有子节点(children),则每个子虚拟节点都执行一遍创建元素的逻辑。

注释节点

    vnode中有一个唯一的标识属性isComment,为true则为注释节点。调用当前环境下的createComment方法(在浏览器环境下就是document.createComment)来创建真实的注释节点并将其插入到指定的父节点中。

文本节点

    tag属性不存在、isComment属性不为true,则为文本节点。调用当前环境下的createTextNode方法(在浏览器环境下就是document.createTextNode)来创建真实的文本节点并将其插入到指定的父节点中。

下图为创建一个节点并将其渲染到视图的全过程:

部分源码如下:  

createElm 

/**
 * 基于 vnode 创建整棵 DOM 树,并插入到父节点上
 * @param vnode
 * @param insertedVnodeQueue  钩子函数队列
 * @param parentElm  参数vnode对应真实 DOM 对象的父节点 DOM 对象
 * @param refElm  占位节点对象,例如,参数vnode对应 DOM 对象的下个兄弟节点
 * @param nested  判断vnode是否是根实例的 Virtual DOM
 * @param ownerArray  自身的子节点数组
 * @param index  子节点数组下标
*/
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

  vnode.isRootInsert = !nested; // for transition enter check
  /**
  * 1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
  *   然后为组件执行各个模块的 create 钩子
  *   如果组件被 keep-alive 包裹,则激活组件
  * 2、如果是一个普通元素,则什么也不做
  */
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // 获取 data 对象
  var data = vnode.data;
  // 所有的孩子节点
  var children = vnode.children;
  var tag = vnode.tag;
  if (isDef(tag)) {
    {
      if (data && data.pre) {
        creatingElmInVPre++;
      }
      if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        );
      }
    }
    // 创建新节点
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
    setScope(vnode);

    /* istanbul ignore if */
    {
      // 递归创建所有子节点(普通元素、组件)
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      // 将节点插入父节点
      insert(parentElm, vnode.elm, refElm);
    }

    if (data && data.pre) {
      creatingElmInVPre--;
    }
  } else if (isTrue(vnode.isComment)) {
    // 注释节点,创建注释节点并插入父节点
    vnode.elm = nodeOps.createComment(vnode.text);
    insert(parentElm, vnode.elm, refElm);
  } else {
    // 文本节点,创建文本节点并插入父节点
    vnode.elm = nodeOps.createTextNode(vnode.text);
    insert(parentElm, vnode.elm, refElm);
  }
}

 createChildren 

// 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
function createChildren (vnode, children, insertedVnodeQueue) {
  // children 是数组,表示是一组节点
  if (Array.isArray(children)) {
    {
      // 检测这组节点的 key 是否重复
      checkDuplicateKeys(children);
    }
    for (var i = 0; i < children.length; ++i) {
      // 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
    }
  } else if (isPrimitive(vnode.text)) {
    // 说明是文本节点,创建文本节点,并插入父节点
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
  }
}

createComponent  

/**
 * 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
 * 然后为组件执行各个模块的 create 方法
 * @param vnode 
 * @param insertedVnodeQueue 数组
 * @param parentElm oldVnode 的父节点
 * @param refElm oldVnode 的下一个兄弟节点
 * @return 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
 */
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    // 验证组件实例是否已经存在并且被 keep-alive 包裹
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    // 执行 vnode.data.init 钩子函数
    // 如果是被 keep-alive 包裹的组件:则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
    // 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */);
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      // 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
      // 这时候就可以给组件执行各个模块的的 create 钩子了
      initComponent(vnode, insertedVnodeQueue);
      // 将组件的 DOM 节点插入到父节点内
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        // 组件被 keep-alive 包裹的情况,激活组件
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true
    }
  }
}

insert  

// 向父节点插入节点 
function insert (parent, elm, ref$$1) {
  if (isDef(parent)) {
    if (isDef(ref$$1)) {
      if (nodeOps.parentNode(ref$$1) === parent) {
        nodeOps.insertBefore(parent, elm, ref$$1);
      }
    } else {
      nodeOps.appendChild(parent, elm);
    }
  }
}

invokeCreateHooks  

// 调用 各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等 
// 然后执行组件的 mounted 生命周期方法
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](emptyNode, vnode);
  }
  // 组件钩子
  i = vnode.data.hook; // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) { i.create(emptyNode, vnode); }
    // 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  }
}

删除节点

当一个节点只在oldVNode中存在时,我们需要把它从DOM中删除。

实现代码如下:

var nodeOps = Object.freeze({
  parentNode: parentNode,
  removeChild: removeChild
})

function parentNode (node) {
  return node.parentNode
}

function removeNode (el) {
  var parent = nodeOps.parentNode(el);
  // element may have already been removed due to v-html / v-text
  if (isDef(parent)) {
    nodeOps.removeChild(parent, el);
  }
}



// 移除指定索引范围(startIdx —— endIdx)内的节点
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    var ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch);
        invokeDestroyHook(ch);
      } else {
        // Text node
        removeNode(ch.elm);
      }
    }
  }
}

更新节点 

两个节点是同一个节点时,才需要更新元素节点。

更新过程:

  1. 判断新旧虚拟节点是否是静态节点,是就直接跳过;
  2. 新虚拟节点有文本属性,并且和旧虚拟节点的文本属性不一样时,可以直接调用setTextContent方法来将视图中DOM节点的内容改为虚拟节点(vnode)的text属性所保存的文字
  3. 新虚拟节点有children属性,如果oldVNode也有children属性,我们要对新旧虚拟节点的children进行一个更详细的对比并更新; 如果oldVNode没有children属性,那么说明oldVNode要么是一个空标签,要么是有文本的文本节点。如果是文本节点,先把文本清空让它变成空标签,然后将vnode中的children挨个创建成真实的DOM元素节点并将其插入到视图中的DOM节点下面
  4. 新虚拟节点无children属性, vnode既没有text属性也没有children属性时,这说明这个新创建的节点是一个空节点,如果oldVNode中有子节点就删除子节点,有文本就删除文本,有什么删什么,最后达到视图中是空标签的目的。

 下图是更新节点的逻辑:

部分源码如下: 

patchVnode 

/**
 * 更新节点
 *   全量的属性更新
 *   如果新老节点都有孩子,则递归执行 diff
 *   如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
 *   如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
 *   更新文本节点
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 老节点和新节点相同,直接返回
  if (oldVnode === vnode) {
    return;
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }

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

  // 异步占位符节点
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }
    return;
  }

  // 跳过静态节点的更新
  // 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }

  // 执行组件的 prepatch 钩子
  var i;
  var data = vnode.data;
  if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
    i(oldVnode, vnode);
  }

  // 老节点的孩子
  var oldCh = oldVnode.children;
  // 新节点的孩子
  var ch = vnode.children;
  // 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
  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)) {
      if (oldCh !== ch) {
        // 如果新老节点都有孩子,则递归执行 diff 过程
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
      }
    } else if (isDef(ch)) {
      // 老孩子不存在,新孩子存在,则创建这些新孩子节点
      {
        checkDuplicateKeys(ch);
      }
      // oldVnode有文本,将文本内容置空
      if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, "");
      }
      // 创建子节点,把vnode子节点添加到DOM中
      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)) {
      // oldVnode有文本,将文本内容置空
      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);
    }
  }
}

 addVnodes

// 在指定索引范围(startIdx —— endIdx)内添加节点
function addVnodes(
  parentElm,
  refElm,
  vnodes,
  startIdx,
  endIdx,
  insertedVnodeQueue
) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(
      vnodes[startIdx],
      insertedVnodeQueue,
      parentElm,
      refElm,
      false,
      vnodes,
      startIdx
    );
  }
}

 下图为源码流程图:

更新子节点

对比两个子节点列表,首先需要做的事情就是循环。循环newChildren(新子节列表),每循环一个新子节点,就去oldChildren(旧子节点列表)中找到和当前节点相同的那个旧子节点。如果在oldChildren中找不到,说明当前子节点是由于状态变化而新增的节点,我们要进行创建节点并插入视图的操作;如果找到了,就执行更新;如果找到的旧子节点的位置与新子节点不同,则执行移动节点操作。

创建子节点

如果在oldChildren中找不到本次循环所指向的新子节点相同的节点,说明当前子节点是由于状态变化而新增的节点 。对于新增节点,我们需要执行创建节点的操作,并将新创建的节点插入到oldChildren中所有未处理节点(未处理就是没有进行任何更新操作的节点)的前面。

更新子节点

当一个节点同时存在于newChildren和oldChildren中才会发生更新操作。

移动子节点

在newChildren中的某个节点和oldChildren中的某个节点是同一个节点,但是位置不同,就需要执行移动节点的操作。

patch运行流程

patch

/**
 * vm.__patch__
 *   1、新节点不存在,老节点存在,调用 destroy,销毁老节点
 *   2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
 *   3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
 */
function patch(oldVnode, vnode, hydrating, removeOnly) {
  // 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) {
      invokeDestroyHook(oldVnode);
    }
    return;
  }

  var isInitialPatch = false;
  var insertedVnodeQueue = [];

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    // 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
    // <div id="app"><comp></comp></div>
    // 这里的 comp 组件初次渲染时就会走这儿
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  } else {
    // 判断 oldVnode 是否为真实元素
    var isRealElement = isDef(oldVnode.nodeType);
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      // 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
    } else {
      // 是真实元素,则表示初次渲染
      if (isRealElement) {
        // 挂载到真实元素以及处理服务端渲染的情况
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR);
          hydrating = true;
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true);
            return oldVnode;
          } else {
            warn(
              "The client-side rendered virtual DOM tree is not matching " +
                "server-rendered content. This is likely caused by incorrect " +
                "HTML markup, for example nesting block-level elements inside " +
                "<p>, or missing <tbody>. Bailing hydration and performing " +
                "full client-side render."
            );
          }
        }
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        // 不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
        oldVnode = emptyNodeAt(oldVnode);
      }

      // 拿到老节点的真实元素
      var oldElm = oldVnode.elm;
      // 获取老节点的父元素,即 body
      var parentElm = nodeOps.parentNode(oldElm);

      // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
      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)
      );

      // update parent placeholder node element, recursively
      // 递归更新父占位符节点元素
      if (isDef(vnode.parent)) {
        var ancestor = vnode.parent;
        var patchable = isPatchable(vnode);
        while (ancestor) {
          for (var i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor);
          }
          ancestor.elm = vnode.elm;
          if (patchable) {
            for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
              cbs.create[i$1](emptyNode, ancestor);
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            var insert = ancestor.data.hook.insert;
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
                insert.fns[i$2]();
              }
            }
          } else {
            registerRef(ancestor);
          }
          ancestor = ancestor.parent;
        }
      }

      // 移除老节点
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0);
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode);
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
}
// 判读两个节点是否相同
function sameVnode(a, b) {
  return (
    // key 必须相同
    a.key === b.key &&
    // 标签相同
    ((a.tag === b.tag &&
      // isComment属性相同(用来判断是否是注释节点)
      a.isComment === b.isComment &&
      // 都有 data 属性
      isDef(a.data) === isDef(b.data) &&
      // input 标签的情况
      sameInputType(a, b)) ||
      // 异步占位符节点
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  );
}
/**
 * diff 过程:
 *   diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
 *             如果没有命中假设,则执行遍历,从老节点中找到新开始节点
 *             找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
 *   如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
 *   如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
 */
function updateChildren(
  parentElm,
  oldCh,
  newCh,
  insertedVnodeQueue,
  removeOnly
) {
  // 老节点的开始索引
  var oldStartIdx = 0;
  // 新节点的开始索引
  var newStartIdx = 0;
  // 老节点的结束索引
  var oldEndIdx = oldCh.length - 1;
  // 第一个老节点
  var oldStartVnode = oldCh[0];
  // 最后一个老节点
  var oldEndVnode = oldCh[oldEndIdx];
  // 新节点的结束索引
  var newEndIdx = newCh.length - 1;
  // 第一个新节点
  var newStartVnode = newCh[0];
  // 最后一个新节点
  var newEndVnode = newCh[newEndIdx];
  var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

  // removeOnly是一个特殊的标志,仅由 <transition-group> 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置
  var canMove = !removeOnly;

  {
    // 检查新节点的 key 是否重复
    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];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 老开始节点和新开始节点是同一个节点,执行 patch
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      // patch 结束后老开始和新开始的索引分别加 1
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 老结束和新结束是同一个节点,执行 patch
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      // patch 结束后老结束和新结束的索引分别减 1
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // 老开始和新结束是同一个节点,执行 patch
      // Vnode moved right
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      // 处理被 transtion-group 包裹的组件时使用
      canMove &&
        nodeOps.insertBefore(
          parentElm,
          oldStartVnode.elm,
          nodeOps.nextSibling(oldEndVnode.elm)
        );
      // patch 结束后老开始索引加 1,新结束索引减 1
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // 老结束和新开始是同一个节点,执行 patch
      // Vnode moved left
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      // patch 结束后,老结束的索引减 1,新开始的索引加 1
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 遍历找到新开始节点在老节点中的位置索引

      if (isUndef(oldKeyToIdx)) {
        // 找到老节点中每个节点 key 和 索引之间的关系映射 => oldKeyToIdx = { key1: idx1, ... }
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      // 在映射中找到新开始节点在老节点中的位置索引
      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
        );
      } else {
        vnodeToMove = oldCh[idxInOld];
        // 如果这两个节点是同一个,则执行 patch
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          // patch 结束后将该老节点置为 undefined
          oldCh[idxInOld] = undefined;
          canMove &&
            nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // 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(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
}

// 检查一组元素的 key 是否重复
function checkDuplicateKeys(children) {
  var seenKeys = {};
  for (var i = 0; i < children.length; i++) {
    var vnode = children[i];
    var key = vnode.key;
    if (isDef(key)) {
      if (seenKeys[key]) {
        warn(
          "Duplicate keys detected: '" +
            key +
            "'. This may cause an update error.",
          vnode.context
        );
      } else {
        seenKeys[key] = true;
      }
    }
  }
}
// 在指定索引范围(startIdx —— endIdx)内添加节点
function addVnodes(
  parentElm,
  refElm,
  vnodes,
  startIdx,
  endIdx,
  insertedVnodeQueue
) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(
      vnodes[startIdx],
      insertedVnodeQueue,
      parentElm,
      refElm,
      false,
      vnodes,
      startIdx
    );
  }
}

// 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
function createKeyToOldIdx(children, beginIdx, endIdx) {
  var i, key;
  var map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) {
      map[key] = i;
    }
  }
  return map;
}

invokeDestroyHook 

/**
 * 销毁节点:
 *   执行组件的 destroy 钩子,即执行 $destroy 方法
 *   执行组件各个模块(style、class、directive 等)的 destroy 方法
 *   如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
 */
function invokeDestroyHook(vnode) {
  var i, j;
  var data = vnode.data;
  if (isDef(data)) {
    if (isDef((i = data.hook)) && isDef((i = i.destroy))) {
      i(vnode);
    }
    for (i = 0; i < cbs.destroy.length; ++i) {
      cbs.destroy[i](vnode);
    }
  }
  if (isDef((i = vnode.children))) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j]);
    }
  }
}

参考资料:

深入浅出Vue.js

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值