diff 算法深入一下?

一、前言

有同學問:能否詳細說一下 diff 算法。

簡單說:diff 算法是一種優化手段,將前後兩個模塊進行差異化比較,修補 (更新) 差異的過程叫做 patch,也叫打補丁。

詳細的說,請閱讀這篇文章,有疑問的地方歡迎聯繫「松寶寫代碼」一起討論。

文章主要解決的問題:

二、爲什麼要說這個 diff 算法?

因爲 diff 算法是 vue2.x , vue3.x 以及 react 中關鍵核心點,理解 diff 算法,更有助於理解各個框架本質。

說到「diff 算法」,不得不說「虛擬 Dom」,因爲這兩個息息相關。

比如:

等等

三、虛擬 dom 的 diff 算法

我們先來說說虛擬 Dom,就是通過 JS 模擬實現 DOM ,接下來難點就是如何判斷舊對象和新對象之間的差異。

Dom 是多叉樹結構,如果需要完整的對比兩棵樹的差異,那麼算法的時間複雜度 O(n ^ 3),這個複雜度很難讓人接收,尤其在 n 很大的情況下,於是 React 團隊優化了算法,實現了 O(n) 的複雜度來對比差異。

實現 O(n) 複雜度的關鍵就是隻對比同層的節點,而不是跨層對比,這也是考慮到在實際業務中很少會去跨層的移動 DOM 元素。

虛擬 DOM 差異算法的步驟分爲 2 步:

3.1 vue 中 diff 算法

實際 diff 算法比較中,節點比較主要有 5 種規則的比較

部分源碼 https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L501 如下:

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;
  }
  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)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode "i");
    if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
  }
  if (isUndef(vnode.text)) {
    // 定義了子節點,且不相同,用diff算法對比
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
      // 新節點有子元素。舊節點沒有
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        // 檢查key
        checkDuplicateKeys(ch);
      }
      // 清空舊節點的text屬性
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
      // 添加新的Vnode
      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);
  }
}

3.2 React diff 算法

在 reconcileChildren 函數的入參中

workInProgress.child = reconcileChildFibers(
  workInProgress,
  current.child,
  nextChildren,
  renderLanes,
);

diff 的兩個主體是:oldFiber(current.child)和 newChildren(nextChildren,新的 ReactElement),它們是兩個不一樣的數據結構。

部分源碼

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
  /* * returnFiber:currentFirstChild的父級fiber節點
   * currentFirstChild:當前執行更新任務的WIP(fiber)節點
   * newChildren:組件的render方法渲染出的新的ReactElement節點
   * lanes:優先級相關
   * */
  // resultingFirstChild是diff之後的新fiber鏈表的第一個fiber。
  let resultingFirstChild: Fiber | null = null;
  // resultingFirstChild是新鏈表的第一個fiber。
  // previousNewFiber用來將後續的新fiber接到第一個fiber之後
  let previousNewFiber: Fiber | null = null;

  // oldFiber節點,新的child節點會和它進行比較
  let oldFiber = currentFirstChild;
  // 存儲固定節點的位置
  let lastPlacedIndex = 0;
  // 存儲遍歷到的新節點的索引
  let newIdx = 0;
  // 記錄目前遍歷到的oldFiber的下一個節點
  let nextOldFiber = null;

  // 該輪遍歷來處理節點更新,依據節點是否可複用來決定是否中斷遍歷
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // newChildren遍歷完了,oldFiber鏈沒有遍歷完,此時需要中斷遍歷
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      // 用nextOldFiber存儲當前遍歷到的oldFiber的下一個節點
      nextOldFiber = oldFiber.sibling;
    }
    // 生成新的節點,判斷key與tag是否相同就在updateSlot中
    // 對DOM類型的元素來說,key 和 tag都相同纔會複用oldFiber
    // 並返回出去,否則返回null
    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes);

    // newFiber爲 null說明 key 或 tag 不同,節點不可複用,中斷遍歷
    if (newFiber === null) {
      if (oldFiber === null) {
        // oldFiber 爲null說明oldFiber此時也遍歷完了
        // 是以下場景,D爲新增節點
        // 舊 A - B - C
        // 新 A - B - C - D oldFiber = nextOldFiber;
      }
      break;
    }
    if (shouldTrackSideEffects) {
      // shouldTrackSideEffects 爲true表示是更新過程
      if (oldFiber && newFiber.alternate === null) {
        // newFiber.alternate 等同於 oldFiber.alternate
        // oldFiber爲WIP節點,它的alternate 就是 current節點
        // oldFiber存在,並且經過更新後的新fiber節點它還沒有current節點,
        // 說明更新後展現在屏幕上不會有current節點,而更新後WIP
        // 節點會稱爲current節點,所以需要刪除已有的WIP節點
        deleteChild(returnFiber, oldFiber);
      }
    }
    // 記錄固定節點的位置
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 將新fiber連接成以sibling爲指針的單向鏈表
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    // 將oldFiber節點指向下一個,與newChildren的遍歷同步移動
    oldFiber = nextOldFiber;
  }

  // 處理節點刪除。新子節點遍歷完,說明剩下的oldFiber都是沒用的了,可以刪除.
  if (newIdx === newChildren.length) {
    // newChildren遍歷結束,刪除掉oldFiber鏈中的剩下的節點
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 處理新增節點。舊的遍歷完了,能複用的都複用了,所以意味着新的都是新插入的了
  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      // 基於新生成的ReactElement創建新的Fiber節點
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      // 記錄固定節點的位置lastPlacedIndex
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 將新生成的fiber節點連接成以sibling爲指針的單向鏈表
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    return resultingFirstChild;
  }
  // 執行到這是都沒遍歷完的情況,把剩餘的舊子節點放入一個以key爲鍵,值爲oldFiber節點的map中
  // 這樣在基於oldFiber節點新建新的fiber節點時,可以通過key快速地找出oldFiber
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // 節點移動
  for (; newIdx < newChildren.length; newIdx++) {
    // 基於map中的oldFiber節點來創建新fiber
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          // 因爲newChildren中剩餘的節點有可能和oldFiber節點一樣,只是位置換了,
          // 但也有可能是是新增的.

          // 如果newFiber的alternate不爲空,則說明newFiber不是新增的。
          // 也就說明着它是基於map中的oldFiber節點新建的,意味着oldFiber已經被使用了,所以需
          // 要從map中刪去oldFiber
          existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
        }
      }

      // 移動節點,多節點diff的核心,這裏真正會實現節點的移動
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 將新fiber連接成以sibling爲指針的單向鏈表
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }
  if (shouldTrackSideEffects) {
    // 此時newChildren遍歷完了,該移動的都移動了,那麼刪除剩下的oldFiber
    existingChildren.forEach((child) => deleteChild(returnFiber, child));
  }
  return resultingFirstChild;
}

四、爲什麼使用虛擬 dom?

很多時候手工優化 dom 確實會比 virtual dom 效率高,對於比較簡單的 dom 結構用手工優化沒有問題,但當頁面結構很龐大,結構很複雜時,手工優化會花去大量時間,而且可維護性也不高,不能保證每個人都有手工優化的能力。至此,virtual dom 的解決方案應運而生。

virtual dom 是 “解決過多的操作 dom 影響性能” 的一種解決方案。

virtual dom 很多時候都不是最優的操作,但它具有普適性,在效率、可維護性之間達到平衡。

virutal dom 的意義:

五、diff 算法的複雜度和特點?

vue2.x 的 diff 位於 patch.js 文件中,該算法來源於 snabbdom,複雜度爲 O(n)。瞭解 diff 過程可以讓我們更高效的使用框架。react 的 diff 其實和 vue 的 diff 大同小異。

最大特點:比較只會在同層級進行, 不會跨層級比較。

<!-- 之前 -->
<div>              <!-- 層級1 -->
  <p>              <!-- 層級2 -->
    <b> aoy </b>   <!-- 層級3 -->
    <span>diff</Span>
  </p>
</div>

<!-- 之後 -->
<div>             <!-- 層級1 -->
  <p>              <!-- 層級2 -->
    <b> aoy </b>   <!-- 層級3 -->
  </p>
  <span>diff</Span>
</div>

對比之前和之後:可能期望將<span>直接移動到<p>的後邊,這是最優的操作。

但是實際的 diff 操作是:

六、vue 的模板文件是如何被編譯渲染的?

vue 中也使用 diff 算法,有必要了解一下 Vue 是如何工作的。通過這個問題,我們可以很好的掌握,diff 算法在整個編譯過程中,哪個環節,做了哪些操作,然後使用 diff 算法後輸出什麼?

解釋:

1、mount 函數

mount 函數主要是獲取 template,然後進入 compileToFunctions 函數。

2、compileToFunction 函數

compileToFunction 函數主要是將 template 編譯成 render 函數。首先讀取緩存,沒有緩存就調用 compile 方法拿到 render 函數的字符串形式,在通過 new Function 的方式生成 render 函數。

// 有緩存的話就直接在緩存裏面拿
const key = options && options.delimiters ? String(options.delimiters) + template : template;
if (cache[key]) {
  return cache[key];
}
const res = {};
const compiled = compile(template, options); // compile 後面會詳細講
res.render = makeFunction(compiled.render); //通過 new Function 的方式生成 render 函數並緩存
const l = compiled.staticRenderFns.length;
res.staticRenderFns = new Array(l);
for (let i = 0; i < l; i++) {
  res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
}

// ......

return (cache[key] = res); // 記錄至緩存中

3、compile 函數

compile 函數將 template 編譯成 render 函數的字符串形式。後面我們主要講解 render

完成 render 方法生成後,會進入到 mount 進行 DOM 更新。該方法核心邏輯如下:

// 觸發 beforeMount 生命週期鉤子
callHook(vm, 'beforeMount');
// 重點:新建一個 Watcher 並賦值給 vm._watcher
vm._watcher = new Watcher(
  vm,
  function updateComponent() {
    vm._update(vm._render(), hydrating);
  },
  noop,
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
  vm._isMounted = true;
  callHook(vm, 'mounted');
}
return vm;

上面提到的 compile 就是將 template 編譯成 render 函數的字符串形式。核心代碼如下:

export function compile(template: string, options: CompilerOptions): CompiledResult {
  const AST = parse(template.trim(), options); //1. parse
  optimize(AST, options); //2.optimize
  const code = generate(AST, options); //3.generate
  return {
    AST,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
}

compile 這個函數主要有三個步驟組成:

分別輸出一個包含

parse 函數:主要功能是將 template 字符串解析成 AST(抽象語法樹)。前面定義的 ASTElement 的數據結構,parse 函數就是將 template 裏的結構(指令,屬性,標籤) 轉換爲 AST 形式存進 ASTElement 中,最後解析生成 AST。

optimize 函數(src/compiler/optomizer.js): 主要功能是標記靜態節點。後面 patch 過程中對比新舊 VNode 樹形結構做優化。被標記爲 static 的節點在後面的 diff 算法中會被直接忽略,不做詳細比較。

generate 函數(src/compiler/codegen/index.js): 主要功能根據 AST 結構拼接生成 render 函數的字符串

const code = AST ? genElement(AST) : '_c("div")';
staticRenderFns = prevStaticRenderFns;
onceCount = prevOnceCount;
return {
  render: `with(this){return ${code}}`, //最外層包一個 with(this) 之後返回
  staticRenderFns: currentStaticRenderFns,
};

其中 genElement 函數(src/compiler/codgen/index.js)是根據 AST 的屬性調用不同的方法生成字符串返回。

總之:

就是 compile 函數中三個核心步驟介紹,

4、patch 函數

patch 函數 就是新舊 VNode 對比的 diff 函數,主要是爲了優化 dom,通過算法使操作 dom 的行爲降低到最低, diff 算法來源於 snabbdom,是 VDOM 思想的核心。snabbdom 的算法是爲了 DOM 操作跨級增刪節點較少的這一目標進行優化, 它只會在同層級進行,不會跨層級比較。

總結一下

七、vue2.x,vue3.x,React 中的 diff 有區別嗎?

總的來說:

在創建 VNode 就確定類型,以及在 mount/patch 的過程中採用位運算來判斷一個 VNode 的類型,在這個優化的基礎上再配合 Diff 算法,性能得到提升。

可以看一下 vue3.x 的源碼: https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js

對 oldFiber 和新的 ReactElement 節點的比對,將會生成新的 fiber 節點,同時標記上 effectTag,這些 fiber 會被連到 workInProgress 樹中,作爲新的 WIP 節點。樹的結構因此被一點點地確定,而新的 workInProgress 節點也基本定型。在 diff 過後,workInProgress 節點的 beginWork 節點就完成了,接下來會進入 completeWork 階段。

八、diff 算法的源頭 snabbdom 算法

snabbdom 算法: https://github.com/snabbdom/snabbdom

定位:一個專注於簡單性、模塊化、強大功能和性能的虛擬 DOM 庫。

1、snabbdom 中定義 Vnode 的類型

snabbdom 中定義 Vnode 的類型 (https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/vnode.ts#L12)

export interface VNode {
  sel: string | undefined; // selector的縮寫
  data: VNodeData | undefined; // 下面VNodeData接口的內容
  children: Array<VNode | string> | undefined; // 子節點
  elm: Node | undefined; // element的縮寫,存儲了真實的HTMLElement
  text: string | undefined; // 如果是文本節點,則存儲text
  key: Key | undefined; // 節點的key,在做列表時很有用
}

export interface VNodeData {
  props?: Props;
  attrs?: Attrs;
  class?: Classes;
  style?: VNodeStyle;
  dataset?: Dataset;
  on?: On;
  attachData?: AttachData;
  hook?: Hooks;
  key?: Key;
  ns?: string; // for SVGs
  fn?: () => VNode; // for thunks
  args?: any[]; // for thunks
  is?: string; // for custom elements v1
  [key: string]: any; // for any other 3rd party module
}

2、init 函數分析

init 函數的地址:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L63

init() 函數接收一個模塊數組 modules 和可選的 domApi 對象作爲參數,返回一個函數,即 patch() 函數。

domApi 對象的接口包含了很多 DOM 操作的方法。

3、patch 函數分析

源碼:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/init.ts#L367

4、h 函數分析

源碼:

https://github.com/snabbdom/snabbdom/blob/27e9c4d5dca62b6dabf9ac23efb95f1b6045b2df/src/h.ts#L33

h() 函數接收多種參數,其中必須有一個 sel 參數,作用是將節點內容掛載到該容器中,並返回一個新 VNode。

九、diff 算法與 snabbdom 算法的差異地方?

在 vue2.x 不是完全 snabbdom 算法,而是基於 vue 的場景進行了一些修改和優化,主要體現在判斷 key 和 diff 部分。

1、在 snabbdom 中 通過 key 和 sel 就判斷是否爲同一節點,那麼在 vue 中,增加了一些判斷 在滿足 key 相等的同時會判斷,tag 名稱是否一致,是否爲註釋節點,是否爲異步節點,或者爲 input 時候類型是否相同等。

https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L35

/**
 * @param a 被對比節點
 * @param b  對比節點
 * 對比兩個節點是否相同
 * 需要組成的條件:key相同,tag相同,是否都爲註釋節點,是否同時定義了data,如果是input標籤,那麼type必須相同
 */
function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  );
}

2、diff 差異,patchVnode 是對比模版變化的函數,可能會用到 diff 也可能直接更新。

https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L404

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;
  const canMove = !removeOnly;

  if (process.env.NODE_ENV !== "production") {
    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(
        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將要移動的節點
        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
          );
        }
      }
      // vnodeToMove將要移動的節點
      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);
  }
}

更多閱讀

引用

後言

數據平臺前端團隊,在公司內負責大數據相關產品的研發。我們在前端技術上保持着非常強的熱情,除了數據產品相關的研發外,在數據可視化、海量數據處理優化、web excel、WebIDE、私有化部署、工程工具都方面都有很多的探索和積累,有興趣可以與我們聯繫。

投遞簡歷,更多精彩文章,歡迎關注 “豆皮範兒”

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Y6eD7RB74mE6ceEGMFBGug