从snabdom看vnode diff原理

110 阅读4分钟

深度优先遍历

主要方法patch

return function patch(
  oldVnode: VNode | Element | DocumentFragment,
  vnode: VNode
): VNode {
  let i: number, elm: Node, parent: Node;
  const insertedVnodeQueue: VNodeQueue = [];
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  if (isElement(api, oldVnode)) {
    oldVnode = emptyNodeAt(oldVnode);
  } else if (isDocumentFragment(api, oldVnode)) {
    oldVnode = emptyDocumentFragmentAt(oldVnode);
  }

  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    elm = oldVnode.elm!;
    parent = api.parentNode(elm) as Node;

    createElm(vnode, insertedVnodeQueue);

    if (parent !== null) {
      api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
  }
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
  return vnode;
};

因snabdom支持把vnode渲染到dom,上,因此若oldVnode为真实dom节点,会先转换成vnode

if (isElement(api, oldVnode)) {
  oldVnode = emptyNodeAt(oldVnode);
} else if (isDocumentFragment(api, oldVnode)) {
  oldVnode = emptyDocumentFragmentAt(oldVnode);
}

附上vnode节点定义:

  • 其中sel的定义可以理解为 tagName(elm).toLowerCase() + id + class
  • data中会包含:style、attr等
    export function vnode(
      sel: string | undefined,
      data: any | undefined,
      children: Array<VNode | string> | undefined,
      text: string | undefined,
      elm: Element | DocumentFragment | Text | undefined
    ): VNode {
      const key = data === undefined ? undefined : data.key;
      return { sel, data, children, text, elm, key };
    }

简单判断两个vnode是否相同,若不相同直接丢弃oldvnode

    function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
      const isSameKey = vnode1.key === vnode2.key;
      const isSameIs = vnode1.data?.is === vnode2.data?.is;
      const isSameSel = vnode1.sel === vnode2.sel;
      const isSameTextOrFragment =
        !vnode1.sel && vnode1.sel === vnode2.sel
          ? typeof vnode1.text === typeof vnode2.text
          : true;

      return isSameSel && isSameKey && isSameIs && isSameTextOrFragment;
    }

    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

vnode不同时处理方法

根据新的vnode创建节点,进入到 createElm(vnode, insertedVnodeQueue); 方法。

若是 children 存在,则递归调用 createElm 方法生成真实的ele节点

if (is.array(children)) {
  for (i = 0; i < children.length; ++i) {
    const ch = children[i];
    if (ch != null) {
      api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
    }
  }
} else if (is.primitive(vnode.text)) {
  api.appendChild(elm, api.createTextNode(vnode.text));
}

实际渲染到dom节点

若是父结点存在,则会先插入现有老节点的后面,再删除老节点

if (parent !== null) {
  api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
  removeVnodes(parent, [oldVnode], 0, 0);
}

patchVnode(vnode相同的处理方法)

这个相对复杂一些,进入到专门的 patchVnode 方法。

elm不变,赋值到新vnode中。

以下进行到判断环节,vnode.text存在可以判断为是纯文本节点:

  • vnode.text不存在
    • isDef(oldCh) && isDef(ch) && oldCh !== ch
      • 进入到 updateChildren(elm, oldCh, ch, insertedVnodeQueue)
    • children 存在
      • 真实渲染到dom, 调用addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    • children 存在
      • 删除dom中的节点, removeVnodes(elm, oldCh, 0, oldCh.length - 1)
  • vnode.text存在,并且oldVnode.text !== vnode.text
    • oldVnode.children存在,执行removeVnodes(elm, oldCh, 0, oldCh.length - 1)方法
    • api.setTextContent(elm, vnode.text!)
    const elm = (vnode.elm = oldVnode.elm)!;

    if (isUndef(vnode.text)) {
        // 两个
        if (isDef(oldCh) && isDef(ch)) {
          if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
        } else if (isDef(ch)) {
          if (isDef(oldVnode.text)) api.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)) {
          api.setTextContent(elm, "");
        }
      } else if (oldVnode.text !== vnode.text) {
        if (isDef(oldCh)) {
          removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        }
        api.setTextContent(elm, vnode.text!);
      }
      hook?.postpatch?.(oldVnode, vnode);
    }

updateChildren方法(更新vnode子节点)

image.png

  1. 如果某个节点不存在,则递增或递减。(空数据跳过,兼容乱序对比)
if (oldStartVnode == null) {
  oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
  oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
  newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
  newEndVnode = newCh[--newEndIdx];
}
顺序对比

开始顺序对比,有以下四种方式:

新老节点的头部与头部对比

新老节点的尾部与尾部对比

老节点的头部与新节点的尾部对比

老节点的尾部与新节点的头部对比

  1. 如果sameVnode(oldStartVnode, newStartVnode) 进入到patchVnode比对 ,节点递增
  2. 如果sameVnode(oldEndVnode, newEndVnode) 进入到patchVnode比对 ,节点递减
else if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
  oldStartVnode = oldCh[++oldStartIdx];
  newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
  patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
  oldEndVnode = oldCh[--oldEndIdx];
  newEndVnode = newCh[--newEndIdx];
}

4. 老节点的头部与新节点的尾部对比,若是vnode相同(节点向右移动),则进入到patchVnode比对

    1. 调用 insertBeforeoldStartVnode 对应的真实节点放在 oldEndVnode 对应的真实节点后边
改变前
oldStartIdx:0oldEndIdx:3
DABC
ABCD
newStartIdx:0newEndIdx:3
真实dom
oldStartIdx:0oldEndIdx:3
ABCD
newStartIdx:0newEndIdx:3
  1. 老节点的尾部与新节点的头部对比,若是vnode相同(节点向左移动),则进入到patchVnode比对
    1. 调用 insertBeforeoldEndVnode 对应的真实节点放在 oldStartVnode 对应的真实节点前边
改变前
oldStartIdx:0oldEndIdx:3
ABCD
DABC
newStartIdx:0newEndIdx:3
真实dom****
oldStartIdx:0oldEndIdx:3
DABC
newStartIdx:0newEndIdx:3
else if (sameVnode(oldStartVnode, newEndVnode)) {
  // Vnode moved right
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
  api.insertBefore(
    parentElm,
    oldStartVnode.elm!,
    api.nextSibling(oldEndVnode.elm!)
  );
  oldStartVnode = oldCh[++oldStartIdx];
  newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
  // Vnode moved left
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
  api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
  oldEndVnode = oldCh[--oldEndIdx];
  newStartVnode = newCh[++newStartIdx];
}
乱序对比

当上方的几种顺序对比都没有进入if的情况下,会走else中的乱序对比逻辑。

let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;

思路如下:

newCh中正序的下一个要处理的节点newStartVnode为基础,去oldCh中找是否有可以复用的数据,有的话和上边顺序对比一样,走patchVnode更新,没有的话走createElm创建新节点

else {
  if (oldKeyToIdx === undefined) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
  }
  idxInOld = oldKeyToIdx[newStartVnode.key as string];
  if (isUndef(idxInOld)) {
    // New element
    api.insertBefore(
      parentElm,
      createElm(newStartVnode, insertedVnodeQueue),
      oldStartVnode.elm!
    );
  } else {
    elmToMove = oldCh[idxInOld];
    if (elmToMove.sel !== newStartVnode.sel) {
      api.insertBefore(
        parentElm,
        createElm(newStartVnode, insertedVnodeQueue),
        oldStartVnode.elm!
      );
    } else {
      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
      oldCh[idxInOld] = undefined as any;
      api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
    }
  }
  newStartVnode = newCh[++newStartIdx];
}
  1. 创建oldKeyToIdx,然后通过newStartVnode.keyidxInOld
function createKeyToOldIdx(
  children: VNode[],
  beginIdx: number,
  endIdx: number
): KeyToIndexMap {
  const map: KeyToIndexMap = {};
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key;
    if (key !== undefined) {
      map[key as string] = i;
    }
  }
  return map;
}


if (oldKeyToIdx === undefined) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];

2. isUndef(idxInOld),如果key不存在,调用createElm方法创建一个真实dom,插入到oldStartVnode.elm!的前面

if (isUndef(idxInOld)) {
  // New element
  api.insertBefore(
    parentElm,
    createElm(newStartVnode, insertedVnodeQueue),
    oldStartVnode.elm!
  );
}

3. 若key存在,取出oldCh[idxInOld]的vnode

    1. if (elmToMove.sel !== newStartVnode.sel)
api.insertBefore(
  parentElm,
  createElm(newStartVnode, insertedVnodeQueue),
  oldStartVnode.elm!
);
  1. 2. elseoldCh[idxInOld] = undefined as any,对应前面 null 的判断
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);

4. newStartVnode = newCh[++newStartIdx];

对比后剩余处理
if (newStartIdx <= newEndIdx) {
  before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
  addVnodes(
    parentElm,
    before,
    newCh,
    newStartIdx,
    newEndIdx,
    insertedVnodeQueue
  );
}
if (oldStartIdx <= oldEndIdx) {
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}