這是我參與更文挑戰的第2天,活動詳情查看: 更文挑戰。javascript
最近恰好看完Vue
源碼中的Diff算法,恰好在參加更文挑戰,就作了一些動圖還有流程圖,圖文並茂地來詳細講一講,Vue
的Diff
算法叭。html
咱們都知道,Vue
中是使用了基於HTML
的模板語法,容許開發者聲明式地將DOM
綁定至底層Vue
實例的數據。而初始化的時候,Vue
就會將該模板語法轉化爲真實DOM
,渲染到頁面中。vue
<template>
<div>{{msg}}</div>
</template>
<script> export.default { data() { return { msg: 'HelloWorld' } } } </script>
複製代碼
但當數據發生變化的時候,Vue
會如何去更新頁面呢?java
若是選擇從新渲染整個DOM
,那必然會引發整個DOM
樹的重繪和重排,而在真實項目中,不可能就跟上面的例子同樣只有一句<div>{{msg}}</div>
。當咱們的頁面很是複雜的狀況下,且修改的數據隻影響到一小部分頁面數據的更新的時候,從新渲染頁面必定是不可取的。node
而這時候,最便捷的方式,就是找到該修改的數據所影響到的DOM
,而後只更新那一個DOM
就能夠了。這就是Vue
更新頁面的方法。git
Vue
在初始化頁面後,會將當前的真實DOM
轉換爲虛擬DOM
(Virtual DOM),並將其保存起來,這裏稱爲oldVnode
。而後當某個數據發變化後,Vue
會先生成一個新的虛擬DOM
——vnode
,而後將vnode
和oldVnode
進行比較,找出須要更新的地方,而後直接在對應的真實DOM
上進行修改。當修改結束後,就將vnode
賦值給oldVnode
存起來,做爲下次更新比較的參照物。github
而這個更新中的難點,也是咱們今天要聊的內容,就是新舊vnode
的比較,也就是咱們常說的Diff
算法。web
前面咱們提到了虛擬DOM
(Virtual DOM),那虛擬DOM
是什麼呢?算法
咱們可能曾經打印過真實DOM
,它實質上是個對象,可是它的元素是很是的多的,即便是很簡單的幾句代碼。json
所以,在真實DOM
下,咱們不太敢隨便去直接操做和改動。
這時候,虛擬DOM
就誕生了。它也是一個對象,而它實際上是將真實DOM
的數據抽取出來,以對象的形式模擬樹形結構,使其更加簡潔明瞭。
虛擬DOM
沒有很固定的模板,每一個框架上的實現都存在差別,可是大部分結構都是相同的。下面咱們就用Vue
的虛擬DOM
舉個例子。
<div id="app">
<p class="text">HelloWorld</p>
</div>
複製代碼
上面的DOM
經過Vue
生成了下面的虛擬DOM
(有刪減),對象中包含了根節點的標籤tag
、key
值,文本信息text
等等,同時也含有elm
屬性存放真實DOM
,同時有個children
數組,存放着子節點,子節點的結構也是一致的。
{
"tag": "div", // 標籤
"key": undefined, // key值
"elm": div#app, // 真實DOM
"text": undefined, // 文本信息
"data": {attrs: {id:"app"}}, // 節點屬性
"children": [{ // 孩子屬性
"tag": "p",
"key": undefined,
"elm": p.text,
"text": undefined,
"data": {attrs: {class: "text"}},
"children": [{
"tag": undefined,
"key": undefined,
"elm": text,
"text": "helloWorld",
"data": undefined,
"children": []
}]
}]
}
複製代碼
當咱們把一些經常使用的信息提取出來,而且使用對象嵌套的形式,去存放子節點信息,從而造成一個虛擬DOM
,這時候咱們用其來進行比較的話,就會比兩個真實DOM
作比較簡單多了。
在Vue
中,有個render
函數,這個函數返回的VNode
就是一個虛擬DOM
。固然,你也可使用virtual-dom或snabbdom去體驗一下虛擬DOM
。
在使用Diff
算法比較兩個節點的時候,只會在同層級進行比較,而不會跨層級比較。
在Vue
中,主要是patch()
、patchVnode()
和updateChildren()
這三個主要方法來實現Diff
的。
Vue
中的響應式數據變化的時候,就會觸發頁面更新函數updateComponent()
(如何觸發能夠經過閱讀Vue
源碼進行學習或者看一下我以前一篇《簡單手寫實現Vue2.x》);updateComponet()
就會調用patch()
方法,在該方法中進行比較是否爲相同節點,是的話執行patchVnode()
方法,開始比較節點差別;而若是不是相同節點的話,則進行替換操做,具體後面會講到;patchVnode()
中,首先是更新節點屬性,而後會判斷有沒有孩子節點,有的話則執行updateChildren()
方法,對孩子節點進行比較;若是沒有孩子節點的話,則進行節點文本內容判斷更新;(文本節點是不會有孩子節點的)updateChildren()
中,會對傳入的兩個孩子節點數組進行一一比較,當找到相同節點的狀況下,調用patchVnode()
繼續節點差別比較。爲了後面更好的看核心代碼,咱們先在前面捋清楚一些函數。
在源碼中會用isDef()
和isUndef()
判斷vnode
是否存在,實質上是判斷vnode
是否是undefined
或null
,畢竟vnode
虛擬DOM是個對象。
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
複製代碼
在源碼中會用sameVnode()
方法去判斷兩個節點是否相同,實質上是經過去判斷key
值,tag
標籤等靜態屬性從而去判斷兩個節點是否爲相同節點。
注意的是,這裏的相同節點不意味着爲相等節點,好比<div>HelloWorld</div>
和<div>HiWorld</div>
爲相同節點,可是它們並不相等。在源碼中是經過vnode1 === vnode2
去判斷是否是爲相等節點。
// 比較是否相同節點
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
function sameInputType(a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
複製代碼
接下來開始看源碼了,看看Vue
是如何實現Diff
算法。
下面的全部代碼都只保留核心的代碼,想看所有代碼能夠去看
Vue
的源碼(patch文件路徑:github.com/vuejs/vue/b…
首先看看patch()
方法,該方法接收新舊虛擬Dom,即oldVnode
,vnode
,這個函數實際上是對新舊虛擬Dom
作一個簡單的判斷,而尚未進入詳細的比較階段。
vnode
是否存在,若是不存在的話,則表明這個舊節點要整個刪除;vnode
存在的話,再判斷oldVnode
是否存在,若是不存在的話,則表明只須要新增整個vnode
節點就能夠;vnode
和oldVnode
都存在的話,判斷二者是否是相同節點,若是是的話,這調用patchVnode
方法,對兩個節點進行詳細比較判斷;oldVnode
實際上是真實Dom
,這是隻須要將vnode
轉換爲真實Dom
而後替換掉oldVnode
,具體就很少講,這不是今天討論的範圍內。// 更新時調用的__patch__
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 判斷新節點是否存在
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode) // 新的節點不存在且舊節點存在:刪除
return
}
// 判斷舊節點是否存在
if (isUndef(oldVnode)) {
// 舊節點不存在且新節點存在:新增
createElm(vnode, insertedVnodeQueue)
} else {
if (sameVnode(oldVnode, vnode)) {
// 比較新舊節點 diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 初始化頁面(此時的oldVnode是個真實DOM)
oldVnode = emptyNodeAt(oldVnode)
}
// 建立新的節點
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
}
}
return vnode.elm
}
複製代碼
在patchVnode()
中,一樣是接收新舊虛擬Dom,即oldVnode
,vnode
;在該函數中,即開始對兩個虛擬Dom
進行比較更新了。
Dom
是否是全等,即沒有任何變更;是的話直接結束函數,不然繼續執行;vnode.text
是否存在,即vnode
是否是文本節點。是的話,只須要更新節點文本既可,不然的話,這繼續比較;vnode
和oldVnode
是否有孩子節點:
updateChildren()
方法,進行比較更新孩子節點;vnode
有孩子節點而oldVnode
沒有的話,則直接新增全部孩子節點,並將該節點文本屬性設爲空;oldVnode
有孩子節點而vnode
沒有的話,則直接刪除全部孩子節點;oldVnode.text
是否有內容,有的話清空內容既可。// 比較兩個虛擬DOM
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
// 若是兩個虛擬DOM同樣,無需比較直接返回
if (oldVnode === vnode) {
return
}
// 獲取真實DOM
const elm = vnode.elm = oldVnode.elm
// 獲取兩個比較節點的孩子節點
const oldCh = oldVnode.children
const ch = vnode.children
// 屬性更新
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) { // 沒有文本 -> 該狀況通常都是有孩子節點
if (isDef(oldCh) && isDef(ch)) { // 新舊節點都有孩子節點 -> 比較子節點
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { // 新節點有孩子節點,舊節點沒有孩子節點 -> 新增
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 若是舊節點有文本內容,將其設置爲空
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) { // 舊節點有孩子節點,新節點沒有孩子節點 -> 刪除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) { // 舊節點有文本,新節點沒有文本 -> 刪除文本
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) { // 新舊節點文本不一樣 -> 更新文本
nodeOps.setTextContent(elm, vnode.text)
}
}
複製代碼
最後就來看看updateChildren
方法了,這個也是最難理解的一部分,因此就先帶你們一步步捋清楚後,手寫一下,再看源碼。
首先這個方法傳入三個比較重要的參數,即parentElm
父級真實節點,便於直接節點操做;oldCh
爲oldVnode
的孩子節點;newCh
爲Vnode
的孩子節點。
oldCh
和newCh
都是一個數組。 這個方法的做用,就是對這兩個數組一一比較,找到相同的節點,執行patchVnode
再次進行比較更新,剩下的少退多補。
這個方法咱們想到最簡單的方法,就是兩個數組進行遍歷匹配,可是這樣子的複雜度是很大的,時間複雜度爲O(NM)
,並且咱們真實項目中,頁面結構是很是龐大和複雜的,因此這個方案是很是耗性能的。
在Vue
中,主要的實現是用四個指針進行實現。四個指針初始位置分別在兩個數組的頭尾。所以咱們先來初始化必要的變量。
let oldStartIdx = 0; // oldCh數組左邊的指針位置
let oldStartVnode = oldCh[0]; // oldCh數組左邊的指針對應的節點
let oldEndIdx = oldCh.length - 1; // oldCh數組右邊的指針位置
let oldEndVnode = oldCh[oldEndIdx]; // oldCh數組右邊的指針對應的節點
let newStartIdx = 0; // newCh數組左邊的指針位置
let newStartVnode = newCh[0]; // newCh數組左邊的指針對應的節點
let newEndIdx = newCh.length - 1; // newCh數組右邊的指針位置
let newEndVnode = newCh[newEndIdx]; // newCh數組右邊的指針對應的節點
複製代碼
然而這四個指針不會一直不動的,它們會進行相互比較,若是比較得出是相同節點後,對應兩個指針就會向另外一側移動,而直至兩兩重合的時候,這個循環也就結束了。
固然看到這裏,你會有不少疑問,但先把疑問記起來,後面都會一一做答的。咱們接着寫一個循環語句。
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// TODO
}
複製代碼
接着咱們開始相互比較。
首先是oldStartVnode
和newStartVnode
進行比較,若是比較相同的話,咱們就能夠執行patchVnode
語句,而且移動oldStartIdx
和newStartIdx
。
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
}
}
複製代碼
若是oldStartVnode
和newStartVnode
匹配不上的話,接下來就是oldEndVnode
和newEndVnode
作比較了。
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
}
複製代碼
但若是兩頭比較和兩尾比較都不是相同節點的話,這時候就開始交叉比較了。首先是oldStartVnode
和newEndVnode
作比較。
但交叉比較的時候若是匹配上的話,就須要注意到一個問題,這時候你不只僅要比較更新節點的內容,你還須要移動節點的位置,所以咱們能夠藉助insertBefore
和nextSibling
的DOM
操做方法去實現,這個自行去學習叭。
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 將oldStartVnode節點移動到對應位置,即oldEndVnode節點的後面
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
}
複製代碼
若是oldStartVnode
和newEndVnode
匹配不上的話,就oldEndVnode
和newStartVnode
進行比較。
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
...
}else if(sameVnode(oldEndVnode, newStartVnode)){
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 將oldEndVnode節點移動到對應位置,即oldStartVnode節點的前面
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
}
複製代碼
此時,若是四種比較方法都匹配不到相同節點的話,咱們就只能使用暴力解法去實現了,也就是針對於newStartVnode
這個節點,咱們去遍歷oldCh
中剩餘的節點,一一匹配。
在Vue
中,咱們知道標籤會有一個屬性——key
值,而在同一級的Dom
中,若是key
有值的話,它必須是惟一的;若是不設值就默認爲undefined
。因此咱們能夠先用key
來配對一下。
咱們能夠先生成一個oldCh
的key->index
的映射表,咱們能夠建立一個函數createKeyToOldIdx
實現,返回的結果用一個變量oldKeyToIdx
去存儲。
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
複製代碼
這時候,若是newStartVnode
存在key
的話,咱們就能夠直接用oldKeyToIdx[newStartVnode.key]
拿到對應舊孩子節點的下標index
。
但若是newStartVnode
沒有key
值的話,就只能經過遍歷oldCh
中剩餘的節點,一一進行匹配獲取對應下標index
,這個也能夠封裝成一個函數去實現。
function findIdxInOld(node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
複製代碼
這時候咱們先繼續手寫代碼。
let oldKeyToIdx, idxInOld;
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
...
}else if(sameVnode(oldEndVnode, newStartVnode)){
...
}else{
// 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
固然,這個狀況下,idxInOld
下標值仍是有可能爲空,這種狀況就表明那個newStartVnode
是一個全新的節點,這時候咱們只須要新增節點就能夠了。
若是idxInOld
不爲空的話,咱們就獲取對應的oldVnode
,而後與newStartVnode
進行比較,若是是相同節點的話,調用patchVnode()
函數, 而且將對應的oldVnode
設置爲undefined
;若是匹配出來時不一樣節點,那就直接建立一個節點既可。
最後,移動一下newStartIdx
。
let oldKeyToIdx, idxInOld, vnodeToMove;
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
...
}else if(sameVnode(oldEndVnode, newStartVnode)){
...
}else{
// 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 若是匹配不到index,則建立新節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 獲取對應的舊孩子節點
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 由於idxInOld是處於oldStartIdx和oldEndIdx之間,所以只能將其設置爲undefined,而不是移動兩個指針
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 若是key相同但節點不一樣,就建立一個新的節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 移動新節點的左邊指針
newStartVnode = newCh[++newStartIdx]
}
}
複製代碼
這裏有個重點,若是咱們匹配到對應的oldVnode
的話,須要將其設置爲undefined
,同時當後面咱們的oldStartIdx
和oldEndIdx
移動後,若是判斷出對應的vnode
爲undefined
時,就須要選擇跳過。
let oldKeyToIdx, idxInOld, vnodeToMove;
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 當oldStartVnode爲undefined的時候,oldStartVnode右移
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
// 當oldEndVnode爲undefined的時候,oldEndVnode左移
oldEndVnode = oldCh[--oldEndIdx]
} else if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
...
}else if(sameVnode(oldEndVnode, newStartVnode)){
...
}else{
...
}
}
複製代碼
到這個時候,咱們已經完成的差很少了,只剩下最後的收尾工做了。
若是這時候,oldCh
的兩個指針已經重疊並越過,而newCh
的兩個指針還未重疊;或者說是相反狀況下。
這時候,若是oldCh
有多餘的vnode
,咱們只須要將其都刪除既可;若是是newCh
有多餘的vnode
,咱們只需新增它們就能夠了。
let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
...
} else if (isUndef(oldEndVnode)) {
...
} else if(sameVnode(oldStartVnode, newStartVnode)){
...
}else if(sameVnode(oldEndVnode, newEndVnode)){
...
}else if(sameVnode(oldStartVnode, newEndVnode)){
...
}else if(sameVnode(oldEndVnode, newStartVnode)){
...
}else{
...
}
if (oldStartIdx > oldEndIdx) {
// 當舊節點左指針已經超過右指針的時候,新增剩餘的新的孩子節點
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 當新節點左指針已經超過右指針的時候,刪除剩餘的舊的孩子節點
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
這時候,咱們就完成了updateChildren()
方法了,總體代碼以下:
// 比較兩組孩子節點
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// 設置首尾4個指針和對應節點
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
// diff查找是所需的變量
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// 循環結束條件:新舊節點的頭尾指針都重合
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 當oldStartVnode爲undefined的時候,oldStartVnode右移
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
// 當oldEndVnode爲undefined的時候,oldEndVnode左移
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 當oldStartVnode與newStartVnode節點相同,對比節點
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 對應兩個指針更新
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 當oldEndVnode與newEndVnode節點相同,對比節點
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 對應兩個指針更新
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 當oldStartVnode與newEndVnode節點相同,對比節點
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 將oldStartVnode節點移動到對應位置,即oldEndVnode節點的後面
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 對應兩個指針更新
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 當oldEndVnode與newStartVnode節點相同,對比節點
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 將oldEndVnode節點移動到對應位置,即oldStartVnode節點的前面
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 對應兩個指針更新
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else { // 暴力解法 使用key匹配
// 遍歷剩餘的舊孩子節點,將有key值的生成index表 <{key: i}>
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 若是newStartVnode存在key,就進行匹配index值;若是沒有key值,遍歷剩餘的舊孩子節點,一一與newStartVnode匹配,相同節點的返回index
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// 若是匹配不到index,則建立新節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 獲取對應的舊孩子節點
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 由於idxInOld是處於oldStartIdx和oldEndIdx之間,所以只能將其設置爲undefined,而不是移動兩個指針
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 若是key相同但節點不一樣,就建立一個新的節點
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// 移動新節點的左邊指針
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 當舊節點左指針已經超過右指針的時候,新增剩餘的新的孩子節點
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 當新節點左指針已經超過右指針的時候,刪除剩餘的舊的孩子節點
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
複製代碼
咱們在前面的sameVnode()
能夠看到,咱們在比較兩個節點是否相同的時候,第一個判斷條件就是vnode.key
;而且在後面使用暴力解法的時候,第一選擇也是經過key
去匹配,而這樣會有什麼好處呢?咱們經過下面一個簡單的例子來解答這個問題叭。
假設咱們此時的新舊節點以下:
<!-- old -->
<div>
<p>A</p>
<p>B</p>
<p>C</p>
</div>
<!-- new -->
<div>
<p>B</p>
<p>C</p>
<p>A</p>
</div>
複製代碼
在上面的例子,咱們能夠看出,<p>A</p>
被移動到最後面去了。
但若是咱們沒有設置key
值的話,經過diff
須要操做Dom
的次數會不少,由於當key
爲undefined
的狀況下,每一個p
標籤其實都是相同節點,所以這是執行diff
的話,它會將第一個A
改爲B
,把第二個B
改爲C
,把第三個C
改爲A
,這時一共操做了三次Dom
。
但若是,咱們分別給對應添加了key
值,經過diff
只需操做一次Dom
,即將第一個節點移動到最後既可。