Vue diff VS React diff

2,669 阅读3分钟

vue diff & React diff

分析一下 两个框架的diff 算法 有很大的不同;

首先 vue 只有一个虚拟dom 对比的话也是虚拟dom之间的对比,

vue 的虚拟dom大概如下:

let vNode = {
    tag:"div",
    children:[
        {
            children:[
                {
                    children:undefined,
                    elm: {
                        data:"虚拟DOM",
                        nodeValue: "虚拟DOM",
                    },
                    text: "虚拟DOM",
                    tag: undefined
                }
            ],
            tag: "h1",
            text: undefined
        }
    ],
    text: undefined,
    data: {
        attrs: {
            id: 'demo'
        }
    }
}

其中 vue 的diff 源码如下:

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(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 {
    // 首尾都没找到相同的,从新数组头一个拿出来,去老数组中寻找相同的
    if (isUndef(oldKeyToIdx)) 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]
      if (sameVnode(vnodeToMove, newStartVnode)) {
        patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        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]
  }
}

// 收尾工作:
// 1.老数组先结束,批量增加
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  // 2.新数组先结束,批量删除
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

因为children 是一个数组 所以vue的patch 算法 是 首尾 四种情况相比较 如果 都不相同, 比 新的vnode 在 old的 虚拟dom有没有 如果有 则 复用 然后改变顺序 如果都没有 直接插入。

最后如果老的数组先结束 把新的 数组元素批量增加 如果新的数组先结束 把新的数组元素批量删除

React diff 算法

react 引入了Fiber 这个概念,这个是一个链表结构,新旧Fiber 的对比 就不是数组之间数据的比较了。

因为 Fiber 树是单链表结构,没有子节点数组这样的数据结构,也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。

如果节点还是单个元素 那就比较简单,就不赘述了,这里主要分析 通过React.createElement 创建的 两个不同数组之间的diff 过程

1.相同位置(index) 进行比较

for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    let newChild = newChildren[newIdx];

    if (!(newChild.key === oldFiber.key && newChild.type === oldFiber.type)) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }

    const newFiber = {
      key: newChild.key,
      type: newChild.type,
      props: newChild.props,
      node: oldFiber.node,
      base: oldFiber,
      return: returnFiber,
      effectTag: UPDATE
    };
    
    if (previousNewFiber === null) {
      returnFiber.child = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    // !
    oldFiber = nextOldFiber;
  }

如果newChild.key === oldFiber.key && newChild.type === oldFiber.type 不相等的话 就直接break 跳出循环

2. 新节点已经遍历完成,如果还剩老节点,直接删除

if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    while (oldFiber) {
      deletions.push({
        ...oldFiber,
        effectTag: DELETION
      });
      oldFiber = oldFiber.sibling;
    }
}

3. 如果老链表遍历完成 或者初次渲染

if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      let newChild = newChildren[newIdx];
      const newFiber = {
        key: newChild.key,
        type: newChild.type,
        props: newChild.props,
        node: null,
        base: null,
        return: returnFiber,
        effectTag: PLACEMENT
      };
      
      if (previousNewFiber === null) {
        returnFiber.child = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber; // 都是指向同一个对象 所以最后是 returnFiber.child.sibling.sibling ....
    }
    return;
  }
  

4.如果节点已经移动 如何复用

oldFiber 是一个链表不好遍历 所以先把老链表转成一个Map

function mapRemainingChildren(returnFiber, currentFirstChild) {
  // Add the remaining children to a temporary map so that we can find them by
  // keys quickly. Implicit (null) keys get added to this set with their index
  // instead.
  const existingChildren = new Map();

  let existingChild = currentFirstChild;
  while (existingChild) {
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

上面生成了一个Map

重新遍历新节点的时候 找一下Map 就可以快速找到 是否可以复用,接下来就开始循环新的数组了。

for (; newIdx < newChildren.length; newIdx++) {
    let newChild = newChildren[newIdx];

    let newFiber = {
      key: newChild.key,
      type: newChild.type,
      props: newChild.props,
      return: returnFiber
      // node: null,
      // base: null,
      // effectTag: PLACEMENT
    };

    // 判断新增还是复用
    let matchedFiber = existingChildren.get(
      newChild.key === null ? newIdx : newChild.key
    );
    if (matchedFiber) {
      // 找到啦
      newFiber = {
        ...newFiber,
        node: matchedFiber.node,
        base: matchedFiber,
        effectTag: UPDATE
      };
      // 找到就要删除链表上的元素,防止重复查找
      shouldTrackSideEffects &&
        existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
    } else {
      newFiber = {
        ...newFiber,
        node: null,
        base: null,
        effectTag: PLACEMENT
      };
    }
    if (previousNewFiber === null) {
      returnFiber.child = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }

上面existingChildren 就是 oldFiber 的Map ,matchedFiber 如果找到就可以复用老的 fiber

总结

整个过程分为4个阶段

  1. 第一遍遍历新fiber 如果相同 就可以复用节点,找到不可复用的直接退出循环

  2. 第一遍 新节点已经遍历完成,如果还剩老节点,直接删除

  3. 如果还有 老节点 没有了 新节点还有 或者 初次渲染 就直接插入

  4. 如果新旧节点的位置 有移动,把oldFiber 按照key 或者 index 放到Map 里,然后遍历新的Fiber 看看有匹配的直接复用。