轉自:https://mp.weixin.qq.com/s/haOUo3EWcu40rVdeeEU5Zg
一,什么是diff?
diff 是什么?diff 就是比較兩個樹,render 會生成兩顆樹,一個新樹 newVnode,一棵舊樹oleVnode。然后兩棵樹進行對比更新差異就是 diff ,全稱是 difference, 在 vue 里面diff 算法就是通過 patch 函數來完成的,所有有的時候也叫 patch 算法。
二、diff 發生的時機
diff 發生在什么時候?我們可以說是在數據更新的時候發生的 diff ,因為數據更新會運行 render 函數得到虛擬的 dom 樹,最后頁面重新渲染。
當組件創建的時候,組件所依賴的屬性或者數據發生了改變的時候,會運行一個函數(下面代碼中的updateComponent),該函數會做兩件事:
* 運行_render 生成一個新的虛擬 dom 樹;
* 運行 _updata, 傳入的_render 生成虛擬的 dom 樹,將他和舊的虛擬 dom 樹來進行對比,最后完成真實 dom 的更新。
核心代碼如下,和原始的代碼有些區別,但是都是差不多的意思:
// vue構造函數 function Vue(){ // ... 其他代碼 var updateComponent = () => { this._update(this._render()); } new Watcher(updateComponent); // ... 其他代碼 }
_render 函數生成一個新的虛擬的dom 樹,然后把他傳遞到 _update 里面,然后再將 updateComponent 傳遞給 watcher ,watcher 會監測函數的執行的過程,檢測函數執行期間用到了哪些響應式數據並且進行依賴收集,關於watcher可以看https://www.cnblogs.com/zhilili/p/14734468.html
三 _update 函數的作用
_update 函數會接受一個vnode 參數,這是新生成的虛擬 dom 樹,同時, _update 函數會通過當前組件的 _vnode 屬性,拿到舊的虛擬 dom 樹,_update 函數首先會給組件的 _vnode 屬性重新賦值,然后讓它指向新樹。用代碼表示如下:
function update(vnode){ // vnode 新樹 // this._vnode 舊樹 this._vnode=vnode }
如果是只是更新虛擬的 dom 樹,這樣就可以了,但是我們是要更新頁面,所以要將 diff 進行樹的節點對比,所以可以保存下舊樹 oldVnode 來進行對比,用代碼表示如下:
<body> <div id='app'></div> <script src="./vue.js"></script> <script> const vm=new Vue({ el:"#app", }) function update(vnode){ let oldVnode = vm._vnode; // 保存舊樹 this._vnode=vnode // 更新新樹 } </script> </body>
對比 oldVnode 和 this._vnode ,更新真實的 dom .
接下來,就會判斷 oldVnode 是否存在:
* 不存在:說明這是第一次加載組件,於是通過內部的 patch 函數,直接遍歷新樹,為每一個節點生成真實的 dom, 然后掛載到每個節點的 elm 屬性上。如下:
<body> <div id='app'></div> <script src="./vue.js"></script> <script> const vm=new Vue({ el:"#app", }) console.log(vm) function update(vnode){ let oldVnode = vm._vnode; // 保存舊樹 this._vnode=vnode // 更新新樹 // 判斷舊樹是否存在 if(!oldVnode){ this.__patch__(vm.$el,vnode) } } </script> </body>
* 存在: 說明之前就已經渲染過這個組件,這個時候就通過內部的 patch 函數,對新舊兩顆樹進行對比,從而完成下面兩個目標:完成對所有真實dom 的最小化處理,讓新樹的節點對應合適的真實 dom 。
四 Patch 函數的節點對比流程
術語解釋:一般看到下面的字眼,就表示如下意思:
【相同】:是指兩個虛擬節點的標簽類型和key值均相同,但是input元素還要看type屬性,這個術語在vue源碼中叫做sameVnode,它是一個函數,用來比較兩個虛擬節點是不是同一個節點。
如下:兩個虛擬節點div是否相同
<div>法醫</div>
<div>前端獵手</div>
標簽類型都是 div ,key值不僅僅可以在 v-for 中,還可以在任何的標簽中,上面兩個div中都沒有key值,所以都為undefined,所以標簽類型和key值都是相同的,不用看內容是否相同,應為他是另一個節點:文本節點。
<div key='a'>法醫</div>
<div key='b'>前端獵手</div>
上面兩個節點是標簽相同,但是key值不相同,所以他們是不同的。
【新建元素】:是指根據一個虛擬節點提供的信息,創建一個真實的dom 元素,同時掛載到虛擬節點的 elm 屬性上。
【銷毀元素】:指 vnode.elm.remove()
【更新】:是指兩個虛擬節點進行對比更新,它僅發生在兩個虛擬節點【相同】的情況下,具體過程稍后描敘。
【對比子節點】:是指兩個虛擬節點的子節點進行比較。
比較詳細流程:
1.根節點進行比較:patch 函數首先對根節點進行比較
如果兩個節點【相同】,則進入【更新】流程:
(1)將舊節點的真實 dom 賦值給新節點: newVnode.elm=oldVnode.elm,舊節點會被垃圾回收系機制回收;
(2)對比舊節點和新節點的屬性,有變化的更新到真實的 dom 中;
(3)當前新舊兩個節點處理完成,開始【對比子節點】
如果兩個節點不【相同】:
(1)新節點【遞歸】,【新建元素】
(2)舊節點【銷毀元素】
2.對比子節點:虛擬 dom 樹已經完成了,就剩修改真實的 dom 了,但是修改真實 dom 的效率是比較耗時的,vue 的原則是能不改就不改,盡量什么也不做,在【對比子節點】時,vue 的一切出發點都是為了:
* 盡量什么也不做
* 不行的話,就僅僅修改元素的屬性
* 還不行的話,盡量移動元素,而不是刪除和創建元素
* 實在不行的話,就創建和刪除元素
對比流程:
圖片說明:
黃色圓圈:表示舊節點和新節點所對應的相同節點類型
數字:表示key值,用來區分是不是同一個節點
藍色方塊:表示對比之前舊節點所對應的真實dom
箭頭:分別表示頭指針和尾指針。
接下來,我們要做的示對比舊子節點和新子節點之間的差異,目標是改變真實的dom,並且將新虛擬子節點對應到真實的 dom 里面去,使用兩個指針分別對應新舊子節點的頭部和尾部。
步驟:
(1)首先對比新樹和舊樹的頭指針,看看兩個節點是否是一樣的,從圖中可以看到是一樣的,如果一樣就進入【更新】,流程:先將舊節點的真實 dom 賦值到新節點(真實 dom 連線到新子節點),然后循環對比新舊節點的屬性,看看有什么不一樣的地方,將有變化的更新到真實的 dom 里面,最后還要采用深度優先(一棵樹的節點走到盡頭,再走另一個節點)的方式遞歸循環這兩個節點是否有子節點,如果存在就繼續比較,這里就當不存在了,灰色當已經處理完成了,然后兩個頭指針向后移動
(2)接下來,就繼續對比兩個頭指針,判斷是否相等,如果相等就執行【更新】,但是,很明顯,上面的兩個頭指針指向的虛擬節點的 key 值不同,一個是2,一個3,這個時候它並不會立即就銷毀dom 重建,因為它會一條路走到黑,一直在舊節點里面找到和它相同的,實在找不到才會創建新的 dom 。這個時候就會對比尾指針,下面可見尾指針指向的新舊節點是相同的,這個時候就會進入【更新】,將舊節點上的真實 dom 賦值給新節點,並且尾指針向前移動一位。
(3)這個時候就會繼續進行比較,又開始比較新舊節點的頭指針,可見頭指針指向的虛擬節點並不相同,它又會去比較尾指針,但是尾指針指向的新舊節點也不同,這個時候,它就會將舊節點的頭指針和新節點的尾指針進行比較,這個時候舊節點的頭指針和新節點的尾指針相同,它會將舊節點頭部的節點對應的真實 dom 賦值給新節點的尾部。
注意:真實dom 的位置必須要和新虛擬節點的位置對應,因此,要進行真實 dom 的位置移動,將真實dom 的位置移動到新虛擬dom的位置上,如下,並且舊虛擬頭部指針向后移動一位,而新虛擬尾部指針向前移動一位。
(4)繼續比對,新舊頭指針不同,尾指針不同,兩個頭尾也不同,然后它會以新樹頭指針為基准,循環舊虛擬子節點,看看新樹圓3是否存在於舊虛擬子節點,存在的話在哪個位置,找到之后進行復用,連線,有變化的地方更新到真實dom,操作跟前面幾步一樣,真實dom也要進行位置移動
,移動到舊樹頭指針之前。隨后新樹頭指針繼續往后移動到圓9位置,如下圖:
(5)繼續比對,新舊頭指針不同,尾指針不同,但新樹頭指針和舊樹尾指針相同,操作跟前面幾步相同,但依然需要進行位置移動,移動到舊樹頭指針之前。隨后新樹頭指針往后移動,與新樹尾指針重合,舊樹尾指針向前移動到圓1位置,如下圖
(6)繼續比對,新舊兩樹頭指針不同,尾指針不同,兩個頭尾也不同,然后它以新樹頭指針為基准,循環舊虛擬子節點,找圓8在舊樹中存不存在,從圖中可以看出,並不存在,這個時候確實沒辦法了,只能 「新建元素」。隨后新樹頭指針繼續向后移動到圓2位置,如圖:
(7)當頭指針移動到圓2位置時,頭指針已經不再是有效的了,當頭指針超過尾指針的時候,循環結束,從過程我們可以看到新樹先循環完成,但是舊樹還有剩余的節點,這說明舊樹中剩余的節點都是應該被刪除的節點,所對應的真實dom也會被移除
最終真實dom生成完畢,整個過程我們只新建了一個元素,如下圖:
在面試的時候也會被問到關於diff算法的問題,以下是參考回答:
當組件創建和更新的時候, vue 會執行內部的 update 函數,該函數使用 render 函數生成虛擬的 dom 樹,找到差異點,最終更新到真實dom
將新舊對比差異的過程叫 diff ,vue 在內部通過一個叫做 patch 的函數來完成該過程。
在對比的過程,vue 采用深度優先,同級比較的方式進行比較,同級比較就是說它不會跨越結構進行比較,在判斷兩個節點是否相同的時候,是根據虛擬節點的 key 和 tag 來進行判斷的。
具體來說,首先對根節點進行對比,如果相同則將舊節點關聯的真實dom的引用掛到新節點上,然后根據需要更新屬性到真實dom,然后再對比其子節點數組;如果不相同,則按照新節點的信息遞歸創建所有真實dom,同時掛到對應虛擬節點上,然后移除掉舊的dom。
在對比其子節點數組時,vue對每個子節點數組使用了兩個指針,分別指向頭尾,然后不斷向中間靠攏來進行對比,這樣做的目的是盡量復用真實dom,盡量少的銷毀和創建真實dom。如果發現相同,則進入和根節點一樣的對比流程,如果發現不同,則移動真實dom到合適的位置。
這樣一直遞歸的遍歷下去,直到整棵樹完成對比。