patch函数分析

253 阅读6分钟

开篇

在生成了vnode之后,就需要生成真实节点并且挂载到页面上了。这时候就用到了patch方法,对比是否是第一次将vnode生成真实节点挂载到页面上。如果已经有之前生成过的vnode了,那么就需要做diff算法,对比两者之间的差异,最小化更新页面。如果没有生成过的vnode,那么就是页面第一次将vnode转换为真实节点挂载到页面上,然后在把原来的节点内容清除。

patch

/**
 * 总共有三种情况要处理
 1.新节点不存在,老节点存在,就把老节点销毁
 2.如果oldVnode是真实节点,就代表是初次渲染,将vnode转换为新的真实节点插入dom中,然后移除之前的oldvnode
 3.如果oldVnode不是真实节点,就代表进入了更新阶段,执行patchVnode方法
 * @param {*} oldVnode 
 * @param {*} vnode 
 * @param {*} hydrating 
 * @param {*} removeOnly 
 * @returns 
 */
function patch(oldVnode, vnode, hydrating, removeOnly) {
  //新的节点不存在
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
    return;
  }
​
  let isInitialPatch = false;
  const insertedVnodeQueue = [];
​
  //老的节点不存在,相当于组件初次渲染
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  } else {
    //判断oldVnode是不是真实节点
    const isRealElement = isDef(oldVnode.nodeType);
    //如果不是真实节点就走patchVnode方法更新节点
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
    } else { //如果是真实节点就先创建节点并且插入到dom中 之后把原先的oldVnode删掉
      if (isRealElement) {
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode);
      }
​
      // replacing existing element
      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)
      );
​
      // 递归更新父占位符节点元素
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent;
        const patchable = isPatchable(vnode);
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor);
          }
          ancestor.elm = vnode.elm;
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor);
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert;
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]();
              }
            }
          } else {
            registerRef(ancestor);
          }
          ancestor = ancestor.parent;
        }
      }
​
      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0);
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode);
      }
    }
  }
​
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm;
}
​

invokeDestroyHook

/**
 * 调用组件的$destroy方法,销毁节点
 * 如果节点还有子节点 递归调用
 * @param {*} vnode 
 */
function invokeDestroyHook(vnode) {
  let i, j;
  const 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]);
    }
  }
}

createElm

/**
 * 根据vnode 创建整个dom节点并插入到父节点上
 * @param {*} vnode 
 * @param {*} insertedVnodeQueue 
 * @param {*} parentElm 
 * @param {*} refElm 
 * @param {*} nested 
 * @param {*} ownerArray 
 * @param {*} index 
 * @returns 
 */
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/*
    如果vnode是一个组件,那么就创建一个组件实例,并挂载到页面上。
    之后执行组件的created钩子
    如果组件被keep-alive包裹,那么就激活组件
    如果不是组件就跳过
  */
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }
​
  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
​
  if (isDef(tag)) {
    
    // 创建新的节点
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
​
    // 设置style scope
    setScope(vnode);
​
    //递归创建所有子节点
    createChildren(vnode, children, insertedVnodeQueue);
    if (isDef(data)) {
      //执行实例的created方法
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    //将节点插入到父节点中
    insert(parentElm, vnode.elm, refElm);
  } 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);
  }
}

insert

/**
 * 将节点插入到父节点中
 * @param {*} parent
 * @param {*} elm
 * @param {*} ref
 */
function insert(parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref);
      }
    } else {
      nodeOps.appendChild(parent, elm);
    }
  }
}

removeVnodes

/**
 * 移除整个vnode,会调用实例上的$destroy方法
 * @param {*} vnodes
 * @param {*} startIdx
 * @param {*} endIdx
 */
function removeVnodes(vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch);
        invokeDestroyHook(ch);
      } else {
        // Text node
        removeNode(ch.elm);
      }
    }
  }
}

nodeOps

/*
dom平台操作方法
*/
export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
​
export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}
​
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}
​
export function createComment (text: string): Comment {
  return document.createComment(text)
}
​
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
​
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}
​
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
​
export function parentNode (node: Node): ?Node {
  return node.parentNode
}
​
export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}
​
export function tagName (node: Element): string {
  return node.tagName
}
​
export function setTextContent (node: Node, text: string) {
  node.textContent = text
}
​
export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

patchVnode

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);
  }
​
  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;
  }
​
  // 如果是静态节点 那么就跳过更新
  // 复用旧节点的实例
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  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)) {
    //调用cbs.update数组里面的方法,全量更新节点(属性,class,监听器等)
    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)
        //更新子节点
        updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
    } else if (isDef(ch)) {
      // 旧的子节点不存在,新的子节点存在,那么就创建这些子节点
      if (process.env.NODE_ENV !== "production") {
        checkDuplicateKeys(ch);
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(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);
  }
}

updateChildren

/**
 * diff算法对比两个节点的差异
 * @param {*} parentElm 
 * @param {*} oldCh 
 * @param {*} newCh 
 * @param {*} insertedVnodeQueue 
 * @param {*} removeOnly 
 */
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") {
    //检查新节点的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)) {   //如果旧节点的开始节点和新节点的开始节点都是同一个节点
      //调用patchVnode方法进行更新
      patchVnode(
        oldStartVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      //移动索引,并且更新旧节点的开始节点
      oldStartVnode = oldCh[++oldStartIdx];
      //移动索引,并且更新新节点的开始节点
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {   //如果旧节点的结束节点和新节点的结束节点都是同一个节点
      //调用patchVnode方法进行更新
      patchVnode(
        oldEndVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
      //移动索引,并且更新旧节点的结束节点
      oldEndVnode = oldCh[--oldEndIdx];
      //移动索引,并且更新新节点的结束节点
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) { //如果旧节点的开始节点和新节点的结束节点都是同一个节点
      // Vnode moved right
      //调用patchVnode方法进行更新
      patchVnode(
        oldStartVnode,
        newEndVnode,
        insertedVnodeQueue,
        newCh,
        newEndIdx
      );
​
      //处理被transition-group包裹的组件
      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方法进行更新
      patchVnode(
        oldEndVnode,
        newStartVnode,
        insertedVnodeQueue,
        newCh,
        newStartIdx
      );
      
      //处理被transition-group包裹的组件
      canMove &&
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      
      //移动索引,并且更新旧节点的结束节点
      oldEndVnode = oldCh[--oldEndIdx];
      //移动索引,并且更新新节点的开始节点
      newStartVnode = newCh[++newStartIdx];
    } else {
      // 如果上面四种条件都不成立,那么就建立每个节点的key到节点之间的映射
      // 先建立老节点的映射关系,key=>index
      if (isUndef(oldKeyToIdx))
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      // 先判断新节点的key是否存在,如果有就直接按新节点的key去映射表里面找到老节点的索引位置。如果不存在就循环遍历找到对应的老节点,提取索引
      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];
        //判断老节点和新节点的开始节点是否都是同一个节点
        if (sameVnode(vnodeToMove, newStartVnode)) {
          //调用patchVnode方法进行更新
          patchVnode(
            vnodeToMove,
            newStartVnode,
            insertedVnodeQueue,
            newCh,
            newStartIdx
          );
          //patch结束后将该老节点置为undefined
          oldCh[idxInOld] = undefined;
          //处理被transition-group包裹的组件
          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(oldCh, oldStartIdx, oldEndIdx);
  }
}

总结

1.patch方法做了什么?

答:

如果新节点不存在,老节点存在,就把老节点销毁

如果老节点是真实节点,就代表页面刚进行初始渲染,需要将新的节点转换为真实节点并从插入到父节点上,然后把老节点移除

如果老节点更新节点都是同样的vnode结构,就代表页面进入了更新阶段,需要调用patchVnode方法进行更新

2.patchVnode方法做了什么?

首先先判断旧节点和新节点是否相同 如果是就返回 不做任何操作

如果两个节点都是静态节点那么复用节点,跳过后面的patch操作

全量更新旧节点的属性

判断新节点是否是文本节点 如果是文本节点就直接更新文本 如果不是就分为几种情况

(1)如果旧节点的子节点和新节点的子节点存在,就走updateChildren方法

(2)如果旧的子节点不存在,新的子节点存在,那么就创建这些子节点

(3)如果旧的子节点存在,新的子节点不存在,那么就将这些子节点全部移除

(4)如果老的子节点是文本,那么就直接清除

3.updateChildren方法做了什么?

首先先遍历两组节点,开始有四个节点进行对比,分别为旧前,旧后,新前,新后节点。

(1)先拿旧前和新前做对比,如果都是同一个节点,那么就调用patchVnode方法进行更新,同时更新旧前和新前的位置

(2)之后拿旧后和新后做对比,如果都是同一个节点,那么就调用patchVnode方法进行更新,同时更新旧后和新后的位置

(3)之后拿旧前和新后做对比,如果都是同一个节点,那么就调用patchVnode方法进行更新,同时更新旧前和新后的位置

(4)之后拿新前和旧后做对比,如果都是同一个节点,那么就调用patchVnode方法进行更新,同时更新新前和旧后的位置

如果上面条件都不成立,那么先建立老节点的映射关系,分别是key=>index。之后拿新节点的key去找对应的老节点,如果没找到说明是新增节点,需要创建节点。如果找到了,那么对比这两个节点是否为同一个节点,如果是同一个节点就走patchVnode方法进行更新,更新完成后将对应引射表的索引清空。如果节点找到了,但是不是同一个节点,那么就需要创建元素,新节点向后移一位。

如果旧前位置已经到了旧后的位置,说明已经遍历完了,但是新节点还没有遍历完,那么就需要创建这些节点。如果新前位置已经到了新后的位置,那么新节点已经遍历完了,老节点还没遍历完,那么就需要把这些老节点全部删除。