diff算法深入一下?


文章轉自豆皮范兒-diff算法深入一下

一、前言

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

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

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

文章主要解決的問題:

  • 1、為什么要說這個 diff 算法?
  • 2、虛擬 dom 的 diff 算法
  • 3、為什么使用虛擬 dom?
  • 4、diff 算法的復雜度和特點?
  • 5、vue 的模板文件是如何被編譯渲染的?
  • 6、vue2.x 和 vue3.x 中的 diff 有區別嗎
  • 7、diff 算法的源頭 snabbdom 算法
  • 8、diff 算法與 snabbdom 算法的差異地方?

二、為什么要說這個 diff 算法?

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

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

比如:

  • vue 的響應式原理?
  • vue 的 template 文件是如何被編譯的?
  • 介紹一下 Virtual Dom 算法?
  • 為什么要用 virtual dom 呢?
  • diff 算法復雜度以及最大的特點?
  • vue2.x 的 diff 算法中節點比較情況?

等等

三、虛擬 dom 的 diff 算法

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

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

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

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

  • 首先從上至下,從左往右遍歷對象,也就是樹的深度遍歷,這一步中會給每個節點添加索引,便於最后渲染差異
  • 一旦節點有子元素,就去判斷子元素是否有不同

3.1 vue 中 diff 算法

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

  • 1、如果新舊 VNode 都是靜態的,同時它們的 key 相同(代表同一節點),並且新的 VNode 是 clone 或者是標記了 once(標記 v-once 屬性,只渲染一次),那么只需要替換 elm 以及 componentInstance 即可。

  • 2、新老節點均有 children 子節點,則對子節點進行 diff 操作,調用 updateChildren,這個 updateChildren 也是 diff 的核心。

  • 3、如果老節點沒有子節點而新節點存在子節點,先清空老節點 DOM 的文本內容,然后為當前 DOM 節點加入子節點。

  • 4、當新節點沒有子節點而老節點有子節點的時候,則移除該 DOM 節點的所有子節點。

  • 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);
    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,
);
  • workInProgress:作為父節點傳入,新生成的第一個 fiber 的 return 會被指向它。
  • current.child:舊 fiber 節點,diff 生成新 fiber 節點時會用新生成的 ReactElement 和它作比較。
  • nextChildren:新生成的 ReactElement,會以它為標准生成新的 fiber 節點。
  • renderLanes:本次的渲染優先級,最終會被掛載到新 fiber 的 lanes 屬性上。

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 的意義:**

  • 1、提供一種簡單對象去代替復雜的 dom 對象,從而優化 dom 操作
  • 2、提供一個中間層,js 去寫 ui,ios 安卓之類的負責渲染,就像 reactNative 一樣。

五、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 操作是:

  • 1、移除<p>里的<span>
  • 2、在創建一個新的<span>插到<p>的后邊。 因為新加的<span>在層級 2,舊的在層級 3,屬於不同層級的比較。

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

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

vue-template.png

解釋:

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;
  • 首先會 new 一個 watcher 對象(主要是將模板與數據建立聯系),在 watcher 對象創建后,

  • 會運行傳入的方法 vm._update(vm._render(), hydrating) 。
    其中的 vm._render()主要作用就是運行前面 compiler 生成的 render 方法,並返回一個 vNode 對象。

  • vm.update() 則會對比新的 vdom 和當前 vdom,並把差異的部分渲染到真正的 DOM 樹上。(watcher 背后的實現原理:vue2.x 的響應式原理)

上面提到的 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,
  • optimize
  • generate

分別輸出一個包含

  • AST 字符串
  • staticRenderFns 的對象字符串
  • render 函數 的字符串。

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 函數中三個核心步驟介紹,

  • compile 之后我們得到 render 函數的字符串形式,后面通過 new Function 得到真正的渲染函數。

  • 數據發生變化后,會執行 watcher 中的_update 函數(src/core/instance/lifecycle.js),_update 函數會執行這個渲染函數,輸出一個新的 VNode 樹形結構的數據。

  • 然后調用 patch 函數,拿到這個新的 VNode 與舊的 VNode 進行對比,只有反生了變化的節點才會被更新到新的真實 DOM 樹上。

4、patch 函數

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

總結一下

  • compile 函數主要是將 template 轉換為 AST,優化 AST,再將 AST 轉換為 render 函數的字符串形式。
  • 再通過 new Function 得到真正的 render 函數,render 函數與數據通過 Watcher 產生關聯。
  • 在數據反生變化的時候調用 patch 函數,執行 render 函數,生成新的 VNode,與舊的 VNode 進行 diff,最終更新 DOM 樹。

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

總的來說:

  • vue2.x 的核心 diff 算法采用雙端比較的算法,同時從新舊 children 的兩端開始進行比較,借助 key 可以復用的節點。

  • vue3.x 借鑒了一些別的算法 inferno(https://github.com/infernojs/inferno) 解決:1、處理相同的前置和后置元素的預處理;2、一旦需要進行 DOM 移動,我們首先要做的就是找到 source 的最長遞增子序列。

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

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

  • react 通過 key 和 tag 來對節點進行取舍,可直接將復雜的比對攔截掉,然后降級成節點的移動和增刪這樣比較簡單的操作。

對 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

  • init() 函數返回了一個 patch() 函數
  • patch() 函數接收兩個 VNode 對象作為參數,並返回一個新 VNode。

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、私有化部署、工程工具都方面都有很多的探索和積累,有興趣可以與我們聯系。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM