詳解Vue2的diff演算法
前言
雙端比較演算法是vue2.x採用的diff演算法,本篇文章只是對雙端比較演算法粗略的過程進行了一下分析,具體細節還是得Vue原始碼,Vue的原始碼在這
過程
假設當前有兩個陣列arr1和arr2
let arr1 = [1,2,3,4,5] let arr2 = [4,5,1,2]
那麼其過程有五步
- arr1[0] 和 arr2[0]比較
- arr1[ arr1.length-1 ] 和 arr2[ arr2.length-1 ] 比較
- arr1[0] 和 arr2[ arr2.length-1 ] 比較
- arr1[ arr1.length-1 ] 和 arr2[0] 比較
- arr2[0] 和 arr1的每個元素進行比較
每次比較都是從陣列的兩端開始比較,如果是首位比較相等,那麼比較的開頭索引+1
如果是在末尾比較成功,那麼比較的結束索引-1,當開頭索引大於結束索引時說明比較已經結束
拆解過程
let arr1 = [1,2] let oldStartIdx = 0 let oldEndIdx = arr1.lenght -1 let newStartIdx = 0 let newEndIdx = arr2.length -1 let oldStartVNode = arr1[oldStartIdx] let oldEndVNode = arr1[oldEndIdx] let newStartVNode = arr2[newStartIdx] let newEndVNode = arr2[newEndIdx] 第一輪: 1. 1和4比較不相等 2. 5和2比較不相等 3. 1和2比較不相等 4. 5和4比較不相等 5. 4和舊陣列逐一比較,和索引為3的值相等,說明4由索引3變換位置為了0, newStartIdx++ //比較完後,使用u_1表示比較成功的元素 [1,u_1,5] //arr1 [u_1,2] //arr2 第二輪: 1. 1和3比較不相等 2. 5和2比較不相等 3. 1和2比較不相等 4. 5和3比較不相等 5. 3和舊陣列逐一比較,和索引為2的值相等,3由索引2變換位置為了0, newStartIdx++ //比較成功後,使用u_2表示比較成功的元素 [1,u_2,2] //arr2 第三輪: 1. 1和5比較不相等 2. 5和2比較不相等 3. 1和2比較不相等 4. 5和5比較相等,5已經從舊陣列oldEndIdx位置移動到了newStartIdx位置,newStartIdx++,oldEndIdx-- 5. 第四步比較成功,進入下一輪 //比較成功後,使用u_3表示比較成功的元素 [1,u_3] //arr1 [u_1,u_3,2] //arr2 第四輪: 1. 1和1比較相等,1已經從舊陣列oldStartIdx位置移動到newStartIdx位置,oldStartIdx++,newStartIdx++ 2. 第一步比較成功,進入下一輪 3. 第一步比較成功,進入下一輪 4. 第一步比較成功,進入下一輪 5. 第一步比較成功,進入下一輪 //比較成功後,使用u_4表示比較成功的元素 [u_4,u_4,2] //arr2 第五輪: 1. 2和2比較相等,newStartIdx++ 2. 第一步比較成功,進入下一輪 3. 第一步比較成功,進入下一輪 4. 第一步比較成功,進入下一輪 5. 第一步比較成功,進入下一輪 //比較成功後,使用u_5表示比較成功的元素 [u_4,u_5,u_5] //arr2
用一個gif圖來表示
上程式碼
function diff(prevChildren,nextChildren) { let oldStartIdx = 0 //舊陣列起始索引 let oldEndIdx = prevChildren.length - 1 //舊陣列結束索引 let newStartIdx = 0 //新陣列其實索引 let newEndIdx = nextChildren.length - 1 //新陣列結束索引 let oldStartVNode = prevChildren[oldStartIdx] let oldEndVNode = prevChildren[oldEndIdx] let newStartVNode = nextChildren[newStartIdx] let newEndVNode = nextChildren[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartVNode) { //undefined 時前移一位 oldStartVNode = prevChildren[++oldStartIdx] } else if (!oldEndVNode) { //undefined 時後移一位 oldEndVNode = prevChildren[--oldEndIdx] } else if (oldStartVNode.key === newStartVNode.key ) { //1.開始與開始 oldStartVNode = prevChildren[++oldStartIdx] newStartVNode = nextChildren[++newStartIdx] } else if ( oldEndVNode.key === newEndVNode.key ) { //2.結束與結束 oldEndVNode = prevChildren[--oldEndIdx] newEndVNode = nextChildren[--newEndIdx] } else if (oldStartVNode.key === newEndVNode.key ) { //3.開始與結束 oldStartVNode = prevChildren[++oldStartIdx] newEndVNode = nextChildren[--newEndIdx] } else if (oldEndVNode.key === newStartVNode.key ) { //4.結束與開始 oldEndVNode = prevChildren[--oldEndIdx] newStartVNode = nextChildren[++newStartIdx] } else { //5.新陣列開頭元素和舊陣列每一個元素對比 const idxInOld = prevChildren.findIndex((node) => { if (node && node.key === newStartVNode.key) { return true } }) if (idxInOld >= 0) { prevChildren[idxInOld] = undefined } else { //newStartVNode是新元素 } newStartVNode = nextChildren[++newStartIdx] } } } diff([1,5],[4,2])
我們發現,上面的演算法走完後,如果新舊兩個陣列只是順序變化,那麼它能完美的diff出差異,但是如果新陣列有新增或者刪除的時候就不行了,因此我們在while迴圈完成後需要找出新增或者刪除的元素,那怎麼知道哪些是新增哪些是刪除的元素呢?
在比較的第五步,選取的新陣列的第一個元素和舊陣列的所有元素逐一對比,這裡我們就可以得出了這個陣列是否是新增,如果對比相等,那就是位置變換,否則當前元素就是新增的,但是,while迴圈的條件是oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,如果是以下情況
let arr1 = [1,5] let arr2 = [1,6,7]
因為迴圈條件的導致,這裡會在5次while後就結束了,因此在陣列末尾的6和7永遠走不了第五步的插入條件,那如何判斷6和7是新增的呢?我們來觀察一下while迴圈結束後的索引
//例子1 let arr1 = [1,7] //diff後它們的索引為 oldStartIdx = 5,oldEndIdx = 4 newStartIdx = 5,newEndIdx = 6 //例子2 let arr1 = [1,7,2] //diff後它們的索引為 oldStartIdx = 3,oldEndIdx = 2 newStartIdx = 6,newEndIdx = 5 //例子3 let arr1 = [1,5] let arr2 = [7,2] //diff後它們的索引為 oldStartIdx = 5,oldEndIdx = 4 newStartIdx = 4,newEndIdx = 4 //例子4 let arr1 = [1,5] let arr2 = [2,6] //diff後它們的索引為 oldStartIdx = 3,newEndIdx = 6
我們發現,新增元素的索引和newStartIdx還有newEndIdx是一一對應的
- 例子1:newStartIdx小於newEndIdx,並且是5和6,而新增元素6對應在arr2的索引為6,新增元素7對應在arr2的索引為7,此時6和7都已經越界出arr1的長度範圍
- 例子2:newStartIdx是大於newEndIdx,沒有對應關係
- 例子3:newStartIdx等於newEndIdx,我們發現arr2索引為4的元素正是新增元素6,但是6次時沒有越界出arr1的長度範圍,它剛好在陣列的最後一個元素
- 例子4:newStartIdx等於newEndIdx,arr2中索引為6的值正是新增元素6
那麼得出的結論就是,如果在while迴圈結束後,如果newStartIdx是小於或者等於newEndIdx,那麼在newStartIdx和newEndIdx索引之間對應的元素就是新增的元素,並且oldStartIdx總是比oldEndIdx大
上面說完了新增,那如果是刪除元素呢?看例子
//例子1 let arr1 = [4,1] let arr2 = [1,oldEndIdx = 4 newStartIdx = 3,newStartIdx = 2 //例子2 let arr1 = [7,4] let arr2 = [5,4] //diff後它們的索引為 oldStartIdx = 0,newStartIdx = 3 //例子3 let arr1 = [1,3] let arr2 = [4,3] //diff後它們的索引為 oldStartIdx = 4,oldEndIdx = 5 newStartIdx = 4,newStartIdx = 3
同理新增的觀察套路,發現newStartIdx總是比newStartIdx大,並且需要刪除的元素總是在oldStartIdx和oldEndIdx對應的索引之間,那麼我們只需要把oldStartIdx和oldEndIdx的元素刪除即可,那問題來了,像例子2 中oldStartIdx和oldEndIdx索引之間的元素有7,6其中真正需要刪除的只有7和6,這樣子不就誤刪了2,5麼?關鍵的來了,我們看例子2的2,5發現它們走的都是雙端比較演算法的第五步,第五步寫的程式碼是
const idxInOld = prevChildren.findIndex((node) => { if (node && node.key === newStartVNode.key) { return true } }) if (idxInOld >= 0) { prevChildren[idxInOld] = undefined } else { //newStartVNode是新元素 } newStartVNode = nextChildren[++newStartIdx]
如果idxInOld>0說明在舊陣列中找到了,那麼我們將preChildren[idxInOld]設定為undefined,也就是說2,5經過diff演算法後,它們在arr1中的值已經被替換為了undefined,這裡也是就為什麼在diff演算法開始需要判斷!oldStartVNode和!oldEndVnode的原因了,下面我們完善程式碼
function diff(prevChildren,nextChildren) { let oldStartIdx = 0 //舊陣列起始索引 let oldEndIdx = prevChildren.length - 1 //舊陣列結束索引 let newStartIdx = 0 //新陣列其實索引 let newEndIdx = nextChildren.length - 1 //新陣列結束索引 let oldStartVNode = prevChildren[oldStartIdx] let oldEndVNode = prevChildren[oldEndIdx] let newStartVNode = nextChildren[newStartIdx] let newEndVNode = nextChildren[newEndIdx] while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartVNode) { //undefined 時前移一位 oldStartVNode = prevChildren[++oldStartIdx] } else if (!oldEndVNode) { //undefined 時後移一位 oldEndVNode = prevChildren[--oldEndIdx] } else if (oldStartVNode.key === newStartVNode.key ) { //1.開始與開始 oldStartVNode = prevChildren[++oldStartIdx] newStartVNode = nextChildren[++newStartIdx] } else if ( oldEndVNode.key === newEndVNode.key ) { //2.結束與結束 oldEndVNode = prevChildren[--oldEndIdx] newEndVNode = nextChildren[--newEndIdx] } else if (oldStartVNode.key === newEndVNode.key ) { //3.開始與結束 oldStartVNode = prevChildren[++oldStartIdx] newEndVNode = nextChildren[--newEndIdx] } else if (oldEndVNode.key === newStartVNode.key ) { //4.結束與開始 oldEndVNode = prevChildren[--oldEndIdx] newStartVNode = nextChildren[++newStartIdx] } else { //5.新陣列開頭元素和舊陣列每一個元素對比 const idxInOld = prevChildren.findIndex((node) => { if (node && node.key === newStartVNode.key) { return true } }) if (idxInOld >= 0) { prevChildren[idxInOld] = undefined } else { //newStartVNode是新元素 } newStartVNode = nextChildren[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { for (; newStartIdx <= newEndIdx; ++newStartIdx) { //新增內容 let vnode = nextChildren[newStartIdx] } } else if (newStartIdx > newEndIdx) { for (let i = oldStartIdx; i <= oldEndIdx; i++) { / /刪除內容 } } } diff([1,2])
接下來我們使用兩個gif圖來表示一下diff過程
1.新增元素
2.減少元素
以上就是詳解Vue2的diff演算法的詳細內容,更多關於Vue2的diff演算法的資料請關注我們其它相關文章!