Vue源码终笔-VNode更新与diff算法初探
寫完這個就差不多了,準備干新項目了。
確實挺不擅長寫東西,感覺都是羅列代碼寫點注釋的感覺,這篇就簡單闡述一下數據變動時DOM是如何更新的,主要講解下其中的diff算法。
?
先來個正常的html模板:
<body><div id='app'><div v-for="item in items">{{item}}</div><div @click='click'>click me!</div></div></body><script src='./vue.js'></script><script>new Vue({el: '#app',data: {items: [1]},methods: {click: function() {this.items.push(2);}}})
頁面上有一個通過v-for渲染的div,還有一個按鈕,點擊按鈕時會讓div數量+1。
首先需要提到的是,每一次渲染DOM,都會保存一份當前虛擬DOM的副本掛載到_vnode屬性上,如圖:
點擊前,整個VNode結構為:根節點及3個子節點,子節點均包含2個div標簽和一個空白文本節點,div包含對應的文本節點。
點擊后,由于vue劫持了部分數組方法,所以會進入自定義的push方法中,將彈入的新元素進行廣播,過程就不看了。
完成數組添加后,會生成一個新的render函數與新的VNode,diff算法就是比較新舊VNode的差異,通過最小的變化操作渲染新的DOM。
講VNode的diff算法之前,有一個小點先講一下:如何判斷當前VNode可復用?
銷毀一個DOM節點并創建一個新的再插入是消耗非常大的,無論是DOM對象本身的復雜性還是操作引起的重繪重排,所以虛擬DOM的目標是盡可能復用現有DOM進行更新。
其中涉及的概念就是新的VNode能否在舊的基礎上修改并復用呢?有一個函數就是做這個判斷的:
function sameVnode(a, b) {return (// key來源于v-for或者自定的:key屬性a.key === b.key &&a.tag === b.tag &&a.isComment === b.isComment &&isDef(a.data) === isDef(b.data) &&sameInputType(a, b))}
該判斷有5重標準:
(1)key:key屬性如果沒有設置默認是undefined,當且僅當v-for的列表渲染中會給節點加一個唯一的key,形式如圖:,key不一樣的節點不進行復用,官方文檔也有說明設置key屬性可以強制重新生成一個新DOM。
(2)tag:復用的節點必須保證標簽名一致,畢竟沒有更改tag名的API
(3)isComment:注釋與普通的DOM不是一個次元,所以需要判斷
(4)isDef(*.data):這個涉及屬性的更新,如果一個節點沒有任何屬性,即data為undefined,與一個有data屬性的節點進行更新不如直接渲染一個新的
(5)sameInputType:這個主要是input標簽type屬性異同判斷,不同的type相當于不同的tag
如果均滿足,可以判定該節點可復用。
?
前面說了,每一個更改數據源,會生成一個新的VNode,來與舊的VNode進行比較,節點間的比較無非是判斷是否可復用,再進行屬性置換。
而diff算法主要是針對子節點的更新,即兩個數組之間的異同比較與更新。
一個數組的變化無非3個狀態:增、刪、改,但是其中增刪會涉及數組索引與對應元素的變動,總體來講還是比較復雜的。
源碼中有一個函數專門處理子節點比較,整體如下:
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {// var...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 舊VNode不存在if (isUndef(oldStartVnode)) {// ...} else if (isUndef(oldEndVnode)) {// ...} else if (sameVnode(oldStartVnode, newStartVnode)) {// ...} else if (sameVnode(oldEndVnode, newEndVnode)) {// ...} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// ...} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// ...} else {// ... }}if (oldStartIdx > oldEndIdx) {// ...} else if (newStartIdx > newEndIdx) {// ... }}
第一次看還是比較懵逼的,主路線while循環中有7重判斷,分別對應7種情況。
分解本例中的情況,不貼代碼,嘗試畫個圖:
對比新舊VNode,可以看出新的VNode在索引0的后面插入了一個新的tag
接下來通過updateChildren函數進行比較,有很多的變量,這里還需要一個圖:
在函數中有8個變量,其中4個舊VNode,4個新VNode,分別是一一對應的,解釋一半就行了:
var oldStartIdx = 0;var newStartIdx = 0;var oldEndIdx = oldCh.length - 1;var oldStartVnode = oldCh[0];var oldEndVnode = oldCh[oldEndIdx];var newEndIdx = newCh.length - 1;var newStartVnode = newCh[0];var newEndVnode = newCh[newEndIdx];
(1)oldStartIdx => 從前往后的舊VNode數組索引,初始化時為0 => 簡稱為前索引
(2)oldStartVnode => 對應索引的舊VNode元素 => 簡稱為前元素
(3)oldEndIdx => 從后往前的舊VNode數組索引,初始化為children的數組長度 => 簡稱為后索引
(4)oldEndVnode => 對應索引的舊Vnode元素 => 簡稱為后元素
后面的闡述全部用簡稱,不然太難講了,并且新VNode的數組簡稱newCh,舊VNode的數組簡稱oldCh
另外4個變量只是將old更換為new,并對應新VNode的索引與元素。
接下來是一個大while循環,終止條件是前索引大于后索引(newCh或oldCh):
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)) { // Vnode moved right// ...} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// ...} else {// ... }}
? 由于有幾種情況我模擬不出來,只能大概過一下。
1、isUndef(oldStartVnode)、isUndef(oldEndVnode)
前兩種是oldCh前元素或oldCh后元素不存在,我能模擬的情況是當oldCh中沒有元素時,會出現這種情況。
這時只是單純加前索引加1或者后索引減1,而oldCh長度此時為0,會立即跳出while循環,進入下一步。
2、sameVnode(a,b)
下面的的4種情況都是判斷節點是否可復用,然后進行更新。其中對比的情況有4對:
oldCh前元素 => newCh前元素
oldCh后元素 => newCh后元素
oldCh前元素 => newCh后元素
oldCh后元素 => newCh前元素?
取第一種情況來說,如果比較通過,說明oldCh前元素可以被復用,隨即調用patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)來對DOM進行更新,由于tag是不變的,可以直接對DOM進行各種API調用,比如說事件更改,只要remove舊事件,add新事件就行,這里只是DOM對象的屬性更改,不會影響到DOM的增刪。
當patch完畢后,會將oldCh前索引及newCh的前索引加1,并更新對應的元素,然后進入下一輪循環。
畫一輪圖解釋:
此時第一個子節點已經更新完畢,然后重新開始對比,如果oldCh與newCh的索引1處也可復用,會再次更新并加1,直到前索引大于后索引時,說明所有可能的比較都進行完畢。
這里的4種比較沒有必要重復過一遍,如果是前索引就加1,后索引就減1。
3、else{...}
最后一種情況是需要強制更新元素時才會有的情況,比如:
<body><div id='app'><div v-if="!vIfIter" key='o'>old Ele1</div><div v-if="vIfIter" key='n'>new Ele</div><div @click='click'>click me!</div></div></body><script src='./vue.js'></script><script>new Vue({el: '#app',data: {vIfIter: false},methods: {click: function() {this.vIfIter = true;}}})</script>
此時,由于設置了單獨的key值,所以div被標記為不可復用,跳過了所有判斷進入了else階段:
// 這里將舊VNode中剩余的元素key值作為對象輸出if (isUndef(oldKeyToIdx)) {oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);}// 判斷新VNode中是否存在可復用的元素idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;// 不存在就創建一個新的插入DOM中if (isUndef(idxInOld)) {// New element }// 存在 else {elmToMove = oldCh[idxInOld];if (sameVnode(elmToMove, newStartVnode)) {// 更新VNode patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);// 把舊的VNode置空 此處會觸發到while循環的前兩個判斷oldCh[idxInOld] = undefined;// 移動更新后的VNodecanMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);newStartVnode = newCh[++newStartIdx];}// 同樣的key值不同的tag 創建新DOM插入else {// same key but different element. treat as new element }}
簡單來講還是可復用就復用,不可復用創建新DOM插入。
?
最后來看看while循環跳出來的語句,其實很簡單:
// VNode數量增加了if (oldStartIdx > oldEndIdx) {// 如果VNode是中間插入就會存在refElm// 否則refElm為null 調用insertBefore會將DOM插入父元素尾部refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);}// 減少了 else if (newStartIdx > newEndIdx) {// 移除多出來的DOM節點 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);}
?
至此,所有的分析完了,上面的案例有興趣可以自己跑跑。
不容易啊,寫完了。。。已經入行5個月,由于沒有什么好項目練手,只能看源碼提升基本功,接下來可能很長時間不寫博客了。(反正也沒人看,啊哈哈哈哈~)
(定個小目標,Codewars刷到3kyu,加油!)
轉載于:https://www.cnblogs.com/QH-Jimmy/p/7449789.html
總結
以上是生活随笔為你收集整理的Vue源码终笔-VNode更新与diff算法初探的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 东京不太热是谁唱的啊?
 - 下一篇: 布偶猫多少钱一只,宠物级、繁育级、赛级有