觀感度:🌟🌟🌟🌟🌟javascript
口味:辣炒花蛤html
烹飪時間:10min前端
本文已收錄在前端食堂同名倉庫Github github.com/Geekhyt,歡迎光臨食堂,若是以爲酒菜還算可口,賞個 Star 對食堂老闆來講是莫大的鼓勵。
想要搞明白 Vue3 的 DOM Diff 核心算法,咱們要從一道 LeetCode 真題提及。vue
咱們先來一塊兒讀讀題:java
給定一個無序的整數數組,找到其中最長上升子序列的長度。git
示例:github
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:算法
進階: 你能將算法的時間複雜度下降到 O(nlogn) 嗎?typescript
讀題結束。數組
首先,咱們須要對基本的概念進行了解和區分:
注意:子序列中元素的相對順序必須保持在原始數組中的相對順序
關於動態規劃的思想,還不瞭解的同窗們能夠移步個人這篇專欄入個門,「算法思想」分治、動態規劃、回溯、貪心一鍋燉
咱們能夠將狀態 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) }
這裏我畫了一張圖,便於你理解。
關於貪心和二分查找還不瞭解的同窗們能夠移步個人這兩篇專欄入個門。
這裏再結合本題理解一下貪心思想,一樣是長度爲 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; };
這裏我畫了一張圖,便於你理解。
注意:這種方式被替換後組成的新數組不必定是解法一中正確結果的數組,但長度是同樣的,不影響咱們對此題求解。
好比這種狀況:[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 核心算法就簡單的多了。
我知道你已經火燒眉毛了,可是這裏仍是要插一句,若是你還不瞭解 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
題解,你就已經掌握了 Vue3
的 DOM 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.點贊、評論、轉發 === 催更!