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

相關文章
相關標籤/搜索