Vue3 DOM Diff 核心算法解析

image

觀感度:🌟🌟🌟🌟🌟javascript

口味:辣炒花蛤html

烹飪時間:10min前端

本文已收錄在前端食堂同名倉庫Github github.com/Geekhyt,歡迎光臨食堂,若是以爲酒菜還算可口,賞個 Star 對食堂老闆來講是莫大的鼓勵。

想要搞明白 Vue3 的 DOM Diff 核心算法,咱們要從一道 LeetCode 真題提及。vue

咱們先來一塊兒讀讀題:java

LeetCode 真題 300. 最長上升子序列

給定一個無序的整數數組,找到其中最長上升子序列的長度。git

示例:github

輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。

說明:算法

  • 可能會有多種最長上升子序列的組合,你只須要輸出對應的長度便可。
  • 你算法的時間複雜度應該爲 O(n2) 。

進階: 你能將算法的時間複雜度下降到 O(nlogn) 嗎?typescript

讀題結束。數組

什麼是上升子序列?

首先,咱們須要對基本的概念進行了解和區分:

  • 子串:必定是連續的
  • 子序列:子序列不要求連續 例如:[6, 9, 12] 是 [1, 3, 6, 8, 9, 10, 12] 的一個子序列
  • 上升/遞增子序列:必定是嚴格上升/遞增的子序列

注意:子序列中元素的相對順序必須保持在原始數組中的相對順序

題解

動態規劃

關於動態規劃的思想,還不瞭解的同窗們能夠移步個人這篇專欄入個門,「算法思想」分治、動態規劃、回溯、貪心一鍋燉

咱們能夠將狀態 dp[i] 定義爲以 nums[i] 這個數結尾(必定包括 nums[i])的最長遞增子序列的長度,並將 dp[i] 初始化爲 1,由於每一個元素都是一個單獨的子序列。

定義狀態轉移方程:

  • 當咱們遍歷 nums[i] 時,須要同時對比已經遍歷過的 nums[j]
  • 若是 nums[i] > nums[j]nums[i] 就能夠加入到序列 nums[j] 的最後,長度就是 dp[j] + 1

注:(0 <= j < i) (nums[j] < nums[i])

const lengthOfLIS = function(nums) {
    let n = nums.length;
    if (n == 0) {
        return 0;
    }
    let dp = new Array(n).fill(1);
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Math.max(...dp) 
}
  • 時間複雜度:O(n^2)
  • 空間複雜度:O(n)

這裏我畫了一張圖,便於你理解。

貪心 + 二分查找

關於貪心和二分查找還不瞭解的同窗們能夠移步個人這兩篇專欄入個門。

這裏再結合本題理解一下貪心思想,一樣是長度爲 2 的序列,[1,2] 必定比 [1,4] 好,由於它更有潛力。換句話說,咱們想要組成最長的遞增子序列,
就要讓這個子序列中上升的儘量的慢,這樣才能更長。

咱們能夠建立一個 tails 數組,用來保存最長遞增子序列,若是當前遍歷的 nums[i] 大於 tails 的最後一個元素(也就是 tails 中的最大值)時,咱們將其追加到後面便可。不然的話,咱們就查找 tails 中第一個大於 nums[i] 的數並替換它。由於是單調遞增的序列,咱們可使用二分查找,將時間複雜度下降到 O(logn)

const lengthOfLIS = function(nums) {
    let len = nums.length;
    if (len <= 1) {
        return len;
    }
    let tails = [nums[0]];
    for (let i = 0; i < len; i++) {
        // 當前遍歷元素 nums[i] 大於 前一個遞增子序列的 尾元素時,追加到後面便可
        if (nums[i] > tails[tails.length - 1]) {
            tails.push(nums[i]);
        } else {
            // 不然,查找遞增子序列中第一個大於當前值的元素,用當前遍歷元素 nums[i] 替換它
            // 遞增序列,可使用二分查找
            let left = 0;
            let right = tails.length - 1;
            while (left < right) {
                let mid = (left + right) >> 1;
                if (tails[mid] < nums[i]) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            tails[left] = nums[i];
        }
    }
    return tails.length;
};
  • 時間複雜度:O(nlogn)
  • 空間複雜度:O(n)

這裏我畫了一張圖,便於你理解。

注意:這種方式被替換後組成的新數組不必定是解法一中正確結果的數組,但長度是同樣的,不影響咱們對此題求解。

好比這種狀況:[1,4,5,2,3,7,0]

  • tails = [1]
  • tails = [1,4]
  • tails = [1,4,5]
  • tails = [1,2,5]
  • tails = [1,2,3]
  • tails = [1,2,3,7]
  • tails = [0,2,3,7]

咱們能夠看到最後 tails 的長度是正確的,可是裏面的值不正確,由於最後一步的替換破壞了子序列的性質。

Vue3 DOM Diff 核心算法

搞清楚了最長遞增子序列這道算法題,咱們再來看 Vue3 的 DOM Diff 核心算法就簡單的多了。

我知道你已經火燒眉毛了,可是這裏仍是要插一句,若是你還不瞭解 React 以及 Vue2 的 DOM Diff 算法能夠移步這個連接進行學習,按部就班的學習可讓你更好的理解。

回來後咱們思考一個問題:Diff 算法的目的是什麼?

爲了減小 DOM 操做的性能開銷,咱們要儘量的複用 DOM 元素。因此咱們須要判斷出是否有節點須要移動,應該如何移動以及找出那些須要被添加或刪除的節點。

好了,進入本文的正題,Vue3 DOM Diff 核心算法。

首先咱們要搞清楚,核心算法的的位置。核心算法實際上是當新舊 children 都是多個子節點的時候纔會觸發。

下面這段代碼就是 Vue3 的 DOM Diff 核心算法,我加上了在源碼中的路徑,方便你查找。

// packages/runtime-core/src/renderer.ts
function getSequence(arr: number[]): number[] {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      while (u < v) {
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

getSequence 的做用就是找到那些不須要移動的元素,在遍歷的過程當中,咱們能夠直接跳過不進行其餘操做。

其實這個算法的核心思想就是咱們上面講到的求解最長遞增子序列的第二種解法,貪心 + 二分查找法。這也是爲何不着急說它的緣由,由於若是你看懂了上面的 LeetCode 題解,你就已經掌握了 Vue3DOM Diff 核心算法的思想啦。

不過,想要搞懂每一行代碼的細節,還需放到 Vue3 整個 DOM Diff 的上下文中去才行。並且須要注意的是,上面代碼中的 getSequence 方法的返回值與 LeetCode 題中所要求的返回值不一樣,getSequence 返回的是最長遞增子序列的索引。上文咱們曾提到過,使用貪心 + 二分查找替換的方式存在一些 Bug,可能會致使結果不正確。Vue3 把這個問題解決掉了,下面咱們來一塊兒看一下它是如何解決的。

// packages/runtime-core/src/renderer.ts
function getSequence(arr: number[]): number[] {
  const p = arr.slice() // 拷貝一個數組 p
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    // 排除等於 0 的狀況
    if (arrI !== 0) {
      j = result[result.length - 1]
      // 與最後一項進行比較
      if (arr[j] < arrI) { 
        p[i] = j // 最後一項與 p 對應的索引進行對應
        result.push(i)
        continue
      }
      // arrI 比 arr[j] 小,使用二分查找找到後替換它
      // 定義二分查找區間
      u = 0
      v = result.length - 1
      // 開啓二分查找
      while (u < v) {
        // 取整獲得當前位置
        c = ((u + v) / 2) | 0
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      // 比較 => 替換
      if (arrI < arr[result[u]]) {
        if (u > 0) { 
          p[i] = result[u - 1]  // 正確的結果
        }
        result[u] = i // 有可能替換會致使結果不正確,須要一個新數組 p 記錄正確的結果
      }
    }
  }
  u = result.length
  v = result[u - 1]
  // 倒敘回溯 用 p 覆蓋 result 進而找到最終正確的索引
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

Vue3 經過拷貝一個數組,用來存儲正確的結果,而後經過回溯賦值的方式解決了貪心 + 二分查找替換方式可能形成的值不正確的問題。

以上就是 Vue3 DOM Diff 的核心算法部分啦,歡迎光臨前端食堂,客官您慢走~

❤️愛心三連擊

1.若是你以爲食堂酒菜還合胃口,就點個贊支持下吧,你的是我最大的動力。

2.關注公衆號前端食堂,吃好每一頓飯!

3.點贊、評論、轉發 === 催更!

相關文章
相關標籤/搜索