此文內容包括如下:vue
介紹diff算法node
移動節點:移動的節點稱爲α,將α對應的真實的DOM節點移動到,α在新列表中的前一個VNode對應的真實DOM的後面
react
添加節點:在新列表中有全新的VNode
節點,在舊列表中找不到的節點須要添加(經過find這個布爾值來查找)面試
移除節點:當舊的節點不在新列表中時,咱們就將其對應的DOM節點移除(經過key來查找肯定是否刪除)算法
不足:從頭至尾單邊比較,容易增長比較次數數組
DOM節點何時須要移動和如何移動,總結以下:markdown
添加節點【oldEndIndex
以及小於了oldStartIndex
】:將剩餘的節點依次插入到oldStartNode
的DOM
以前post
移除節點【newEndIndex
小於newStartIndex
】:將舊列表剩餘的節點刪除便可學習
區別優化
vue2和vue3的比較:都用了雙端指針
vue3和react比較:vue3在判斷是否須要移動,使用了react的遞增法
幾個算法看下來,套路就是找到移動的節點,而後給他移動到正確的位置。把該加的新節點添加好,把該刪的舊節點刪了,整個算法就結束了。
從頭至尾
遍歷比較,新列表的節點在舊列表中的位置是不是遞增 若是遞增,不須要移動,不然須要移動。
經過key在舊節點中找到新節點的節點,因此key必定要表明惟一性。
生成的
DOM
節點插入到哪裏?
將α對應的真實的DOM節點移動到,α在新列表中的前一個VNode對應的真實DOM的後面
。
將DOM-B移到DOM-D的後面
爲何這麼移動?
首先咱們列表是從頭至尾
遍歷的。這就意味着對於當前VNode
節點來講,該節點以前的全部節點都是排好序的,若是該節點須要移動,那麼只須要將DOM節點移動到前一個vnode
節點以後就能夠,由於在新列表中vnode
的順序就是這樣的。
VNode
節點,在舊列表中找不到的節點須要添加如何發現全新的節點?
定義一個find
變量值爲false
。若是在舊列表找到了key
相同的vnode
,就將find
的值改成true
。當遍歷結束後判斷find
值,若是爲false
,說明當前節點爲新節點
生成的
DOM
節點插入到哪裏?
分兩種狀況:
新列表中的前一個VNode對應的真實DOM的後面
。移動原理同移動節點,也就是由於該節點以前已經排好序。function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i],
find = false;
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
find = true
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 移動節點:移動到前一個節點的後面
let refNode = nextChildren[i - 1].el.nextSibling;
parent.insertBefore(nextChild.el, refNode)
} else {
// 不須要移動節點,記錄當前位置,與以後的節點進行對比
lastIndex = j
}
break
}
}
if (!find) {
// 定義了find變量,插入新節點
let refNode = i <= 0
? prevChildren[0].el
: nextChildren[i - 1].el.nextSibling
mount(nextChild, parent, refNode);
}
}
//移除節點
for (let i = 0; i < prevChildren.length; i++) {
let prevChild = prevChildren[i],
key = prevChild.key,
has = nextChildren.find(item => item.key === key);
if (!has) parent.removeChild(prevChild.el)
}
}
複製代碼
O(m*n)
,有不足,可優化咱們能夠用空間換時間,把key
與index
的關係維護成一個Map
,從而將時間複雜度下降爲O(n)
function reactdiff(prevChildren, nextChildren, parent) {
let prevIndexMap = {},
nextIndexMap = {};
for (let i = 0; i < prevChildren.length; i++) {
let { key } = prevChildren[i]
//保存舊列表key和指引i的關係
prevIndexMap[key] = i
}
let lastIndex = 0;
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i],
nextKey = nextChild.key,
// 經過新列表的key獲得舊列表的指引
j = prevIndexMap[nextKey];
//保存新列表key和指引i的關係
nextIndexMap[nextKey] = i
if (j === undefined) {
//添加節點
let refNode = i === 0
? prevChildren[0].el
: nextChildren[i - 1].el.nextSibling;
mount(nextChild, parent, refNode)
} else {
patch(prevChildren[j], nextChild, parent)
if (j < lastIndex) {
//移動節點:移動到前一個節點的後面
let refNode = nextChildren[i - 1].el.nextSibling;
parent.insertBefore(nextChild.el, refNode)
} else {
// 不須要移動節點,記錄當前位置,與以後的節點進行對比
lastIndex = j
}
}
}
//刪除節點
for (let i = 0; i < prevChildren.length; i++) {
let { key } = prevChildren[i]
if (!nextIndexMap.hasOwnProperty(key)) parent.removeChild(prevChildren[i].el)
}
}
複製代碼
根據reactDiff
的思路,咱們須要先將DOM-A
移動到DOM-C
以後,而後再將DOM-B
移動到DOM-A
以後,完成Diff
。可是咱們經過觀察能夠發現,只要將DOM-C
移動到DOM-A
以前就能夠完成Diff
。
這是由於react只能從頭至尾遍歷,增長了移動次數。因此這裏是有可優化的空間的,接下來咱們介紹vue2.x
中的diff
算法——雙端比較
,該算法解決了上述的問題
雙端比較
就是新列表和舊列表兩個列表的頭與尾互相對比,,在對比的過程當中指針會逐漸向內靠攏,直到某一個列表的節點所有遍歷過,對比中止。
按照如下四個步驟進行對比
oldStartNode
與新列表的頭一個節點newStartNode
對比oldEndNode
與新列表的最後一個節點newEndNode
對比oldStartNode
與新列表的最後一個節點newEndNode
對比oldEndNode
與新列表的頭一個節點newStartNode
對比經過圖形記住1-4的比較順序,先先後雙豎再首尾兩交叉,記住這張圖就夠了
具體規則和移動規則,這裏是重中之重,必定要學習
oldStartNode
與新列表的頭一個節點newStartNode
對比時key
相同。那麼舊列表的頭指針oldStartIndex
與新列表的頭指針newStartIndex
同時向後移動一位。本來在舊列表中就是頭節點,在新列表中也是頭節點,
該節點不須要移動
,因此什麼都不須要作
oldEndNode
與新列表的最後一個節點newEndNode
對比時key
相同。那麼舊列表的尾指針oldEndIndex
與新列表的尾指針newEndIndex
同時向前移動一位。本來在舊列表中就是尾節點,在新列表中也是尾節點,說明
該節點不須要移動
,因此什麼都不須要作
oldStartNode
與新列表的最後一個節點newEndNode
對比時key
相同。那麼舊列表的頭指針oldStartIndex
向後移動一位;新列表的尾指針newEndIndex
向前移動一位。本來舊列表中是頭節點,而後在新列表中是尾節點。那麼
只要在舊列表中把當前的節點移動到本來尾節點的後面
,就能夠了
oldEndNode
與新列表的頭一個節點newStartNode
對比時key
相同。那麼舊列表的尾指針oldEndIndex
向前移動一位;新列表的頭指針newStartIndex
向後移動一位。本在舊列表末尾的節點,倒是新列表中的開頭節點,沒有人比他更靠前,由於他是第一個,因此
只須要把當前的節點移動到本來舊列表中的第一個節點以前,讓它成爲第一個節點
便可。
DOM節點何時須要移動和如何移動,總結以下:
固然也有特殊狀況,下面繼續
咱們只能拿新列表的第一個節點去舊列表中找與其key
相同的節點
找節點的時候有兩種狀況:
移動找到的節點,移動到開頭
DOM移動後,由咱們將舊列表中的節點改成undefined
,這是相當重要的一步,由於咱們已經作了節點的移動了因此咱們不須要進行再次的對比了。最後咱們將頭指針newStartIndex
向後移一位。
直接建立一個新的節點放到最前面就能夠了,而後後移頭指針newStartIndex
。
oldEndIndex
小於了oldStartIndex
,可是新列表中還有剩餘的節點,咱們只須要將剩餘的節點依次插入到oldStartNode
的DOM
以前就能夠了。爲何是插入oldStartNode
以前呢?緣由是剩餘的節點在新列表的位置是位於oldStartNode
以前的,若是剩餘節點是在oldStartNode
以後,oldStartNode
就會先行對比,這個須要思考一下,其實仍是與第四步
的思路同樣。
當新列表的newEndIndex
小於newStartIndex
時,咱們將舊列表剩餘的節點刪除便可。這裏咱們須要注意,舊列表的undefind
。前面提到過,當頭尾節點都不相同時,咱們會去舊列表中找新列表的第一個節點,移動完DOM節點後,將舊列表的那個節點改成undefind
。因此咱們在最後的刪除時,須要注意這些undefind
,遇到的話跳過當前循環便可。
function vue2diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
newStartIndex = 0,
oldStartIndex = prevChildren.length - 1,
newStartIndex = nextChildren.length - 1,
oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldStartIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newStartIndex];
//循環結束條件
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
if (oldStartNode === undefined) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefined) {
oldEndNode = prevChildren[--oldStartIndex]
} else if (oldStartNode.key === newStartNode.key) {
// 頭-頭:不移動
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
// 尾-尾:不移動
patch(oldEndNode, newEndNode, parent)
oldStartIndex--
newStartIndex--
oldEndNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldStartNode.key === newEndNode.key) {
// 頭-尾: 插入到舊節點的尾節點的後面
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newStartIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newStartNode.key) {
// 尾-頭:插入到舊列表的第一個節點以前
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldStartIndex--
newStartIndex++
oldEndNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else {
//特殊狀況
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child && (child.key === newKey));
if (oldIndex === -1) {
mount(newStartNode, parent, oldStartNode.el)
} else {
let prevNode = prevChildren[oldIndex]
patch(prevNode, newStartNode, parent)
parent.insertBefore(prevNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartIndex++
newStartNode = nextChildren[newStartIndex]
}
}
if (newStartIndex > newStartIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
parent.removeChild(prevChildren[oldStartIndex++].el)
}
} else if (oldStartIndex > oldStartIndex) {
while (newStartIndex <= newStartIndex) {
mount(nextChildren[newStartIndex++], parent, oldStartNode.el)
}
}
}
複製代碼
雙端比較,while循環,兩端是向內靠攏的 頭-頭
尾-尾
j是頭向內靠攏指針;
prevEnd是尾向內靠攏指針
j > prevEnd
且j <= nextEnd
【證實新列表有多餘的】j > nextEnd
【證實舊列表有多餘的】上圖,j > prevEnd
且j <= nextEnd
,只須要把新列表中j
到nextEnd
之間剩下的節點插入進去。
若是j > nextEnd
【證實舊列表有多餘的】時,把舊列表中j
到prevEnd
之間的節點刪除
根據新列表剩餘的節點數量,建立一個source
數組,並將數組填滿-1
。
建立數組和對象創建關係:
source
計算出它的最長遞增子序列
用於移動DOM節點】:新節點在舊列表的位置存儲在該數組中,節點key
與指引i
的關係,再經過key去舊列表中去找位置若是舊節點在新列表中沒有的話,直接刪除就好
let prevStart = j,
nextStart = j,
nextLeft = nextEnd - nextStart + 1, // 新列表中剩餘的節點長度
source = new Array(nextLeft).fill(-1), // 建立數組,填滿-1
nextIndexMap = {}, // 新列表節點與index的映射
patched = 0; // 已更新過的節點的數量
// 保存映射關係
for (let i = nextStart; i <= nextEnd; i++) {
let key = nextChildren[i].key
nextIndexMap[key] = i
}
// 去舊列表找位置
for (let i = prevStart; i <= prevEnd; i++) {
let prevNode = prevChildren[i],
prevKey = prevNode.key,
nextIndex = nextIndexMap[prevKey];
// 新列表中沒有該節點 或者 已經更新了所有的新節點,直接刪除舊節點
if (nextIndex === undefind || patched >= nextLeft) {
parent.removeChild(prevNode.el)
continue
}
// 找到對應的節點
let nextNode = nextChildren[nextIndex];
patch(prevNode, nextNode, parent);
// 給source賦值
source[nextIndex - nextStart] = i
patched++
}
}
複製代碼
在找節點時要注意,若是舊節點在新列表中沒有的話,直接刪除就好。除此以外,咱們還須要一個數量表示記錄咱們已經patch
過的節點,若是數量已經與新列表剩餘的節點數量同樣,那麼剩下的舊節點
就直接刪除
若是是全新的節點的話,其在source數組中對應的值就是初始的-1
,經過這一步能夠區分出來哪一個爲全新的節點,哪一個是可複用的。
判斷是否要移動?遞增法,同react思路:若是找到的
index
是一直遞增的,說明不須要移動任何節點。咱們經過設置一個變量move來保存是否須要移動的狀態。
function vue3Diff(prevChildren, nextChildren, parent) {
//...
outer: {
// ...
}
// 邊界狀況的判斷
if (j > prevEnd && j <= nextEnd) {
// ...
} else if (j > nextEnd && j <= prevEnd) {
// ...
} else {
let prevStart = j,
nextStart = j,
nextLeft = nextEnd - nextStart + 1, // 新列表中剩餘的節點長度
source = new Array(nextLeft).fill(-1), // 建立數組,填滿-1
nextIndexMap = {}, // 新列表節點與index的映射
patched = 0,
move = false, // 是否移動
lastIndex = 0; // 記錄上一次的位置
// 保存映射關係
for (let i = nextStart; i <= nextEnd; i++) {
let key = nextChildren[i].key
nextIndexMap[key] = i
}
// 去舊列表找位置
for (let i = prevStart; i <= prevEnd; i++) {
let prevNode = prevChildren[i],
prevKey = prevNode.key,
nextIndex = nextIndexMap[prevKey];
// 新列表中沒有該節點 或者 已經更新了所有的新節點,直接刪除舊節點
if (nextIndex === undefind || patched >= nextLeft) {
parent.removeChild(prevNode.el)
continue
}
// 找到對應的節點
let nextNode = nextChildren[nextIndex];
patch(prevNode, nextNode, parent);
// 給source賦值
source[nextIndex - nextStart] = i
patched++
// 遞增方法,判斷是否須要移動
if (nextIndex < lastIndex) {
move = false
} else {
lastIndex = nextIndex
}
}
if (move) {
// 須要移動
} else {
//不須要移動
}
}
}
複製代碼
怎麼移動?
一旦須要進行DOM移動,咱們首先要作的就是找到source
的最長遞增子序列。
從後向前進行遍歷source
每一項。此時會出現三種狀況:
-1
,這說明該節點是全新的節點,又因爲咱們是從後向前遍歷,咱們直接建立好DOM節點插入到隊尾就能夠了。最長遞增子序列
中的值,也就是i === seq[j]
,這說說明該節點不須要移動最長遞增子序列
中的值,那麼說明該DOM節點須要移動,這裏也很好理解,咱們也是直接將DOM節點插入到隊尾就能夠了,由於隊尾是排好序的。function vue3Diff(prevChildren, nextChildren, parent) {
//...
if (move) {
const seq = lis(source); // [0, 1]
let j = seq.length - 1; // 最長子序列的指針
// 從後向前遍歷
for (let i = nextLeft - 1; i >= 0; i--) {
let pos = nextStart + i, // 對應新列表的index
nextNode = nextChildren[pos], // 找到vnode
nextPos = pos + 1, // 下一個節點的位置,用於移動DOM
refNode = nextPos >= nextChildren.length ? null : nextChildren[nextPos].el, //DOM節點
cur = source[i]; // 當前source的值,用來判斷節點是否須要移動
if (cur === -1) {
// 狀況1,該節點是全新節點
mount(nextNode, parent, refNode)
} else if (cur === seq[j]) {
// 狀況2,是遞增子序列,該節點不須要移動
// 讓j指向下一個
j--
} else {
// 狀況3,不是遞增子序列,該節點須要移動
parent.insetBefore(nextNode.el, refNode)
}
}
} else {
//不須要移動: 咱們只須要判斷是否有全新的節點【其在source數組中對應的值就是初始的-1】,給他添加進去
for (let i = nextLeft - 1; i >= 0; i--) {
let cur = source[i]; // 當前source的值,用來判斷節點是否須要移動
if (cur === -1) {
let pos = nextStart + i, // 對應新列表的index
nextNode = nextChildren[pos], // 找到vnode
nextPos = pos + 1, // 下一個節點的位置,用於移動DOM
refNode = nextPos >= nextChildren.length ? null : nextChildren[nextPos].el, //DOM節點
mount(nextNode, parent, refNode)
}
}
}
}
複製代碼
source
計算出它的最長遞增子序列
用於移動DOM節點】:新節點在舊列表的位置存儲在該數組中,節點key
與指引i
的關係,再經過key去舊列表中去找位置j > nextEnd
舊節點
就直接刪除了【patch標記已更新過的節點的數量】j > prevEnd
且j <= nextEnd
若是是全新的節點的話,其在source數組中對應的值就是初始的-1
,新增最長遞增子序列
中的值,那麼說明該DOM節點須要移動最長遞增子序列是爲了操做移動DOM
對比規則:
第一步:對比新老節點數組的頭頭和尾尾 在這一步將兩頭兩尾相同的進行 patch 第二步:頭尾 patch 結束以後,查看新老節點數組是否是有其中一方已經 patch 完了,假如是,那麼就多刪少補 第三步:遍歷老節點,看老節點是否在新節點裏面存在,假如不存在,就刪除。 // 假如新的子節點都被遍歷完了,那麼就表明說老的數組以後的,都是須要被刪除的 第四步:獲取最長遞增子序列
介紹diff算法
移動節點:移動的節點稱爲α,將α對應的真實的DOM節點移動到,α在新列表中的前一個VNode對應的真實DOM的後面
添加節點:在新列表中有全新的VNode
節點,在舊列表中找不到的節點須要添加(經過find這個布爾值來查找)
移除節點:當舊的節點不在新列表中時,咱們就將其對應的DOM節點移除(經過key來查找肯定是否刪除)
不足:從頭至尾單邊比較,容易增長比較次數
DOM節點何時須要移動和如何移動,總結以下:
添加節點【oldEndIndex
以及小於了oldStartIndex
】:將剩餘的節點依次插入到oldStartNode
的DOM
以前
移除節點【newEndIndex
小於newStartIndex
】:將舊列表剩餘的節點刪除便可
區別
vue2和vue3的比較:都用了雙端指針
vue3和react比較:vue3在判斷是否須要移動,使用了react的遞增法;react是單端比較,這樣移動效率下降,vue3是使用雙端比較
幾個算法看下來,套路就是找到移動的節點,而後給他移動到正確的位置。把該加的新節點添加好,把該刪的舊節點刪了,整個算法就結束了。
此文借鑑別人的文章,梳理成本身的筆記,分別分析了react、vue二、vue3的diff算法實現原理和具體實現,同時比較了這3種算法,應對面試確定不會懼怕。固然總結它不只僅爲了之後的面試,也爲了提高算法思想。
最長遞增子序列可使用動態規劃方法 juejin.cn/post/696278…