一、可能性(常見):
1.
舊的:a b c
新的:a b c d
2.
舊的: a b c
新的:d a b c
3.
舊的:a b c d
新的:a b c
4.
舊的:d a b c
新的: a b c
5.
舊的:a b c d e i f g
新的:a b e c d h f g
對應的真實虛擬節點(為方便理解,文中用字母代替):
// vnode物件
const a = {
type: 'div', // 標籤
props: {style: {color: 'red'}}, // 屬性
children: [], // 子元素
key: 'key1', // key
el: '<div style="color: 'red'"></div>', // 真實dom節點
...
}
二、找規律
去掉前面和後面相同的部分
// c1表示舊的子節點,c2表示新的子節點
const patchKeyedChildren = (c1, c2) => {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 從前面比
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
// 標籤和key是否相同
// if (n1.type === n2.type && n1.key === n2.key)
if (n1 === n2) {
// 繼續對比其屬性和子節點
} else {
break
}
i++
}
// 從後面比
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
// 標籤和key是否相同
// if (n1.type === n2.type && n1.key === n2.key)
if (n1 === n2) {
// 繼續對比其屬性和子節點
} else {
break
}
e1--
e2--
}
console.log(i, e1, e2)
}
// 呼叫示例
patchKeyedChildren(['a', 'b', 'c', 'd'], ['a', 'b', 'c'])
通過這個函式可以得到:
1.
舊的:a b c
新的:a b c d
i = 3 e1 = 2 e2 = 3
2.
舊的: a b c
新的:d a b c
i = 0 e1 = -1 e2 = 0
3.
舊的:a b c d
新的:a b c
i = 3 e1 = 3 e2 = 2
4.
舊的:d a b c
新的: a b c
i = 0 e1 = 0 e2 = -1
5.
舊的:a b c d e i f g
新的:a b e c d h f g
i = 2 e1 = 5 e2 = 5
擴充套件:
舊的:a b c
新的:a b c d e f
i = 3 e1 = 2 e2 = 5
舊的:a b c
新的:a b c
i = 3 e1 = 2 e2 = 2
舊的:e d a b c
新的: a b c
i = 0 e1 = 1 e2 = -1
舊的:c d e
新的:e c d h
i = 0 e1 = 2 e2 = 3
從上面結果中我們可以找到規律:
- 當i大於e1時,只需新增新的子節點
- 當i大於e2時,只需刪除舊的子節點
// 當i大於e1時
if (i > e1) {
if (i <= e2) {
while (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < c2.length ? c2[nextPos].el : null
// 新增新的子節點c2[i]在anchor的前面
// todo
i++
}
}
}
// 當i大於e2時
else if (i > e2) {
if (i <= e1) {
while (i <= e1) {
// 刪除舊的子節點c1[i]
// todo
i++
}
}
}
- 其它,特殊處理
// 其它
let s1 = i
let s2 = i
// 以新的子節點作為參照物
const keyToNewIndexMap = new Map()
for (let i = s2; i <= e2; i++) {
// 節點的key做為唯一值
// keyToNewIndexMap.set(c2[i].key, i)
keyToNewIndexMap.set(c2[i], i)
}
// 新的總個數
const toBePatched = e2 - s2 + 1
// 記錄新子節點在舊子節點中的索引
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
// 迴圈老的子節點
for (let i = s1; i <= e1; i++) {
const oldChild = c1[i]
// let newIndex = keyToNewIndexMap.get(oldChild.key)
let newIndex = keyToNewIndexMap.get(oldChild)
// 在新子節點中不存在
if (newIndex === undefined) {
// 刪除oldChild
// todo
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1 // 永遠不會等於0, 這樣0就可以表示需要建立了
// 繼續對比其屬性和子節點
// todo
}
}
console.log(newIndexToOldIndexMap)
// 需要移動位置
for (let i = toBePatched - 1; i >= 0; i--) {
let index = i + s2
let current = c2[index]
let anchor = index + 1 < c2.length ? c2[index + 1].el : null
if (newIndexToOldIndexMap[i] === 0) {
// 在anchor前面插入新的節點current
// todo
} else {
// 在anchor前面插入對應舊節點.el,current.el元素等於對應的舊節點.el(在其它程式碼中賦值了)
// todo
}
}
第1種和第2種比較簡單,不做過多的講解,我們來看看第3種,以下面為例
序號: 0 1 2 3 4 5 6 7
舊的:(a b) c d e i (f g)
新的:(a b) e c d h (f g)
- 前面a b和後面f g是一樣的,會去掉,只剩中間亂序部分
- 以新的節點為參照物,迴圈舊的節點,從舊的節點中去掉新的沒有的節點,如i
- 標記舊的中沒有的節點,沒有就為0,表示需要建立;有就序號+1,表示可以複用
標記: 4+1 2+1 3+1 0
新的:(...) e c d h (...)
- 從後往前循壞,h為0,建立,放在它下一個f前面;d不為0,複用舊的中的d,放在h前面;c不為0,複用舊的中的c,放在d前面;e不為0,複用舊的中的e,放在c前面
到目的為止,新舊元素的更替已經全部完成,但大家有沒有發現一個問題,e c d h四個元素都移動了一次,我們可以看出如果只移動e和建立h,c和d保持不變,效率會更高
三、演算法優化
1.
序號: 0 1 2 3 4 5 6 7
舊的:(a b) c d e i (f g)
新的:(a b) e c d h (f g)
對應的標記是[5, 3, 4, 0]
2.
序號:0 1 2 3 4 5
舊的:c d e i f g
新的:e c d f g j
對應的標記是[3, 1, 2, 5, 6, 0]
從上面兩個例子中可以看出:
第1個的最優解是找到c d,只需移動e,建立h
第2個的最優解是找到c d f g,只需移動e,建立j
過程:
- 從標記中找到最長的遞增子序列
- 通過最長的遞增子序列找到對應的索引值
- 通過索引值找到對應的值
注意0表示直接建立,不參與計算
例子:
- [3, 1, 2, 5, 6, 0]的最長的遞增子序列為[1, 2, 5, 6],
- 對應的索引為[1, 2, 3, 4],
- 然後我們遍歷e c d f g j,標記中為0的,比如j,直接建立;c d f g索引分別等於1 2 3 4,保持不變;e等於0,移動
// 需要移動位置
// 找出最長的遞增子序列對應的索引值,如:[5, 3, 4, 0] -> [1, 2]
let increment = getSequence(newIndexToOldIndexMap)
console.log(increment)
let j = increment.length - 1
for (let i = toBePatched - 1; i >= 0; i--) {
let index = i + s2
let current = c2[index]
let anchor = index + 1 < c2.length ? c2[index + 1].el : null
if (newIndexToOldIndexMap[i] === 0) {
// 在anchor前面插入新的節點current
// todo
} else {
if (i !== increment[j]) {
// 在anchor前面插入對應舊節點.el,current.el元素等於對應的舊節點.el(在其它程式碼中賦值了)
// todo
} else { // 不變
j--
}
}
}
最長的遞增子序列
// 最長的遞增子序列,https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr) {
const len = arr.length
const result = [0] // 以第一項為基準
const p = arr.slice() // 標記索引,slice為淺複製一個新的陣列
let resultLastIndex
let start
let end
let middle
for (let i = 0; i < len; i++) {
let arrI = arr[i]
if (arrI !== 0) { // vue中等於0,表示需要建立
resultLastIndex = result[result.length - 1]
// 插到末尾
if (arr[resultLastIndex] < arrI) {
result.push(i)
p[i] = resultLastIndex // 前面的那個是誰
continue
}
// 遞增序列,二分類查詢
start = 0
end = result.length - 1
while(start < end) {
middle = (start + end) >> 1 // 相當於Math.floor((start + end)/2)
if (arr[result[middle]] < arrI) {
start = middle + 1
} else {
end = middle
}
}
// 找到最近的哪一項比它大的,替換
if (arr[result[end]] > arrI) {
result[end] = i
if (end > 0) {
p[i] = result[end - 1] // 前面的那個是誰
}
}
}
}
let i = result.length
let last = result[i - 1]
while(i-- > 0) {
result[i] = last
last = p[last]
}
return result
}
console.log(getSequence([5, 3, 4, 0])) // [1, 2]
console.log(getSequence([3, 1, 2, 5, 6, 0])) // [ 1, 2, 3, 4 ]
講解後續補充...
完整程式碼
// 最長的遞增子序列,https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr) {
const len = arr.length
const result = [0] // 以第一項為基準
const p = arr.slice() // 標記索引,slice為淺複製一個新的陣列
let resultLastIndex
let start
let end
let middle
for (let i = 0; i < len; i++) {
let arrI = arr[i]
if (arrI !== 0) { // vue中等於0,表示需要建立
resultLastIndex = result[result.length - 1]
// 插到末尾
if (arr[resultLastIndex] < arrI) {
result.push(i)
p[i] = resultLastIndex // 前面的那個是誰
continue
}
// 遞增序列,二分類查詢
start = 0
end = result.length - 1
while(start < end) {
middle = (start + end) >> 1 // 相當於Math.floor((start + end)/2)
if (arr[result[middle]] < arrI) {
start = middle + 1
} else {
end = middle
}
}
// 找到最近的哪一項比它大的,替換
if (arr[result[end]] > arrI) {
result[end] = i
if (end > 0) {
p[i] = result[end - 1] // 前面的那個是誰
}
}
}
}
let i = result.length
let last = result[i - 1]
while(i-- > 0) {
result[i] = last
last = p[last]
}
return result
}
// c1表示舊的子節點,c2表示新的子節點
const patchKeyedChildren = (c1, c2) => {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 從前面比
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
// 標籤和key是否相同
// if (n1.type === n2.type && n1.key === n2.key)
if (n1 === n2) {
// 繼續對比其屬性和子節點
} else {
break
}
i++
}
// 從後面比
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
// 標籤和key是否相同
// if (n1.type === n2.type && n1.key === n2.key)
if (n1 === n2) {
// 繼續對比其屬性和子節點
} else {
break
}
e1--
e2--
}
console.log(i, e1, e2)
// 當i大於e1時
if (i > e1) {
if (i <= e2) {
while (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < c2.length ? c2[nextPos].el : null
// 新增子節點c2[i]在anchor的前面
// todo
i++
}
}
}
// 當i大於e2時
else if (i > e2) {
if (i <= e1) {
while (i <= e1) {
// 刪除子節點c1[i]
// todo
i++
}
}
}
// 其它
else {
let s1 = i
let s2 = i
// 以新的子節點作為參照物
const keyToNewIndexMap = new Map()
for (let i = s2; i <= e2; i++) {
// 節點的key做為唯一值
// keyToNewIndexMap.set(c2[i].key, i)
keyToNewIndexMap.set(c2[i], i)
}
// 新的總個數
const toBePatched = e2 - s2 + 1
// 記錄新子節點在舊子節點中的索引
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
// 迴圈老的子節點
for (let i = s1; i <= e1; i++) {
const oldChild = c1[i]
// let newIndex = keyToNewIndexMap.get(oldChild.key)
let newIndex = keyToNewIndexMap.get(oldChild)
// 在新子節點中不存在
if (newIndex === undefined) {
// 刪除oldChild
// todo
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1 // 永遠不會等於0, 這樣0就可以表示需要建立了
// 繼續對比其屬性和子節點
// todo
}
}
console.log(newIndexToOldIndexMap)
// 需要移動位置
// 找出最長的遞增子序列對應的索引值,如:[5, 3, 4, 0] -> [1, 2]
let increment = getSequence(newIndexToOldIndexMap)
console.log(increment)
let j = increment.length - 1
for (let i = toBePatched - 1; i >= 0; i--) {
let index = i + s2
let current = c2[index]
let anchor = index + 1 < c2.length ? c2[index + 1].el : null
if (newIndexToOldIndexMap[i] === 0) {
// 在anchor前面插入新的節點current
// todo
} else {
if (i !== increment[j]) {
// 在anchor前面插入對應舊節點.el,current.el元素等於對應的舊節點.el(在其它程式碼中賦值了)
// todo
} else { // 不變
j--
}
}
}
}
}
// 呼叫示例
patchKeyedChildren(['c', 'd', 'e', 'i', 'f', 'g'], ['e', 'c', 'd', 'f', 'g', 'j'])