Vue diff VS React diff

vue diff & React diff

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

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

vue 的虚拟dom大概以下:node

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 源码以下:react

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 的对比 就不是数组之间数据的比较了。markdown

由于 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 跳出循环dom

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 是一个链表很差遍历 因此先把老链表转成一个Mapide

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 看看有匹配的直接复用。

相关文章
相关标签/搜索