經過對 Vue2.0 源碼閱讀,想寫一寫本身的理解,能力有限故從尤大佬2016.4.11第一次提交開始讀,準備陸續寫:javascript
其中包含本身的理解和源碼的分析,儘可能通俗易懂!因爲是2.0的最先提交,因此和最新版本有不少差別、bug,後續將陸續補充,敬請諒解!包含中文註釋的Vue源碼已上傳...vue
先說一下爲何會有虛擬dom比較這一階段,咱們知道了Vue是數據驅動視圖(數據的變化將引發視圖的變化),但你發現某個數據改變時,視圖是局部刷新而不是整個從新渲染,如何精準的找到數據對應的視圖並進行更新呢?那就須要拿到數據改變先後的dom結構,找到差別點並進行更新!java
虛擬dom實質上是針對真實dom提煉出的簡單對象。就像一個簡單的div包含200多個屬性,但真正須要的可能只有tagName
,因此對真實dom直接操做將大大影響性能!
node
簡化後的虛擬節點(vnode)大體包含如下屬性:git
{ tag: 'div', // 標籤名 data: {}, // 屬性數據,包括class、style、event、props、attrs等 children: [], // 子節點數組,也是vnode結構 text: undefined, // 文本 elm: undefined, // 真實dom key: undefined // 節點標識 }
虛擬dom的比較,就是找出新節點(vnode)和舊節點(oldVnode)之間的差別,而後對差別進行打補丁(patch)。大體流程以下github
整個過程仍是比較簡單的,新舊節點若是不類似,直接根據新節點建立dom;若是類似,先是對data比較,包括class、style、event、props、attrs等,有不一樣就調用對應的update函數,而後是對子節點的比較,子節點的比較用到了diff算法,這應該是這篇文章的重點和難點吧。算法
值得注意的是,在Children Compare
過程當中,若是找到了類似的childVnode
,那它們將遞歸進入新的打補丁過程。segmentfault
此次的源碼解析寫簡潔一點,寫太多發現本身都不肯意看 (┬_┬)api
先來看patch()
函數:數組
function patch (oldVnode, vnode) { var elm, parent; if (sameVnode(oldVnode, vnode)) { // 類似就去打補丁(增刪改) patchVnode(oldVnode, vnode); } else { // 不類似就整個覆蓋 elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } return vnode.elm; }
patch()
函數接收新舊vnode兩個參數,傳入的這兩個參數有個很大的區別:oldVnode的elm
指向真實dom,而vnode的elm
爲undefined...但通過patch()
方法後,vnode的elm
也將指向這個(更新過的)真實dom。
判斷新舊vnode是否類似的sameVnode()
方法很簡單,就是比較tag和key是否一致。
function sameVnode (a, b) { return a.key === b.key && a.tag === b.tag; }
對於新舊vnode不一致的處理方法很簡單,就是根據vnode建立真實dom,代替oldVnode中的elm
插入DOM文檔。
對於新舊vnode一致的處理,就是咱們前面常常說到的打補丁了。具體什麼是打補丁?看看patchVnode()
方法就知道了:
function patchVnode (oldVnode, vnode) { // 新節點引用舊節點的dom let elm = vnode.elm = oldVnode.elm; const oldCh = oldVnode.children; const ch = vnode.children; // 調用update鉤子 if (vnode.data) { updateAttrs(oldVnode, vnode); updateClass(oldVnode, vnode); updateEventListeners(oldVnode, vnode); updateProps(oldVnode, vnode); updateStyle(oldVnode, vnode); } // 判斷是否爲文本節點 if (vnode.text == undefined) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { api.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text) } }
打補丁其實就是調用各類updateXXX()
函數,更新真實dom的各個屬性。每一個的update函數都相似,就拿updateAttrs()
舉例看看:
function updateAttrs (oldVnode, vnode) { let key, cur, old const elm = vnode.elm const oldAttrs = oldVnode.data.attrs || {} const attrs = vnode.data.attrs || {} // 更新/添加屬性 for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { if (booleanAttrsDict[key] && cur == null) { elm.removeAttribute(key) } else { elm.setAttribute(key, cur) } } } // 刪除新節點不存在的屬性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key) } } }
屬性(Attribute
)的更新函數的大體思路就是:
setAttribute()
修改;removeAttribute()
刪除。你會發現裏面有個booleanAttrsDict[key]
的判斷,是用於判斷在不在布爾類型屬性字典中。
['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ......]eg:
<video autoplay></video>
,想關閉自動播放,須要移除該屬性。
全部數據比較完後,就到子節點的比較了。先判斷當前vnode是否爲文本節點,若是是文本節點就不用考慮子節點的比較;如果元素節點,就須要分三種狀況考慮:
後面兩種狀況都比較簡單,咱們直接對第一種狀況,子節點的比較進行分析。
子節點比較這部分代碼比較多,先說說原理後面再貼代碼。先看一張子節點比較的圖:
圖中的oldCh
和newCh
分別表示新舊子節點數組,它們都有本身的頭尾指針oldStartIdx
,oldEndIdx
,newStartIdx
,newEndIdx
,數組裏面存儲的是vnode,爲了容易理解就用a,b,c,d等代替,它們表示不一樣類型標籤(div,span,p)的vnode對象。
子節點的比較實質上就是循環進行頭尾節點比較。循環結束的標誌就是:舊子節點數組或新子節點數組遍歷完,(即 oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
)。大概看一下循環流程:
oldStartIdx++
&& newStartIdx++
),真實dom不變,進入下一次循環;不類似,進入第二步。oldEndIdx--
&& newEndIdx--
),真實dom不變,進入下一次循環;不類似,進入第三步。oldStartIdx++
&& newEndIdx--
),未確認dom序列中的頭移到尾,進入下一次循環;不類似,進入第四步。oldEndIdx--
&& newStartIdx++
),未確認dom序列中的尾移到頭,進入下一次循環;不類似,進入第五步。newStartIdx++
);不然,vnode對應的dom(vnode[newStartIdx].elm
)插入當前真實dom序列的頭部,新頭指針後移(即 newStartIdx++
)。先看看沒有key的狀況,放個動圖看得更清楚些!
相信看完圖片有更好的理解到diff算法的精髓,整個過程仍是比較簡單的。上圖中一共進入了6次循環,涉及了每一種狀況,逐個敘述一下:
a
),dom不改變,新舊頭指針均後移。a
節點確認後,真實dom序列爲:a,b,c,d,e,f
,未確認dom序列爲:b,c,d,e,f
;f
),dom不改變,新舊尾指針均前移。f
節點確認後,真實dom序列爲:a,b,c,d,e,f
,未確認dom序列爲:b,c,d,e
;b
),當前剩餘真實dom序列中的頭移到尾,舊頭指針後移,新尾指針前移。b
節點確認後,真實dom序列爲:a,c,d,e,b,f
,未確認dom序列爲:c,d,e
;e
),當前剩餘真實dom序列中的尾移到頭,舊尾指針前移,新頭指針後移。e
節點確認後,真實dom序列爲:a,e,c,d,b,f
,未確認dom序列爲:c,d
;g
節點插入後,真實dom序列爲:a,e,g,c,d,b,f
,未確認dom序列爲:c,d
;h
節點插入後,真實dom序列爲:a,e,g,h,c,d,b,f
,未確認dom序列爲:c,d
;但結束循環後,有兩種狀況須要考慮:
newStartIdx > newEndIdx
)。那就須要把多餘的舊dom(oldStartIdx -> oldEndIdx
)都刪除,上述例子中就是c,d
;oldStartIdx > oldEndIdx
)。那就須要把多餘的新dom(newStartIdx -> newEndIdx
)都添加。上面說了這麼多都是沒有key的狀況,說添加了:key
能夠優化v-for
的性能,究竟是怎麼回事呢?由於v-for
大部分狀況下生成的都是相同tag
的標籤,若是沒有key標識,那麼至關於每次頭頭比較都能成功。你想一想若是你往v-for
綁定的數組頭部push數據,那麼整個dom將所有刷新一遍(若是數組每項內容都不同),那加了key
會有什麼幫助呢?這邊引用一張圖:
有key
的狀況,其實就是多了一步匹配查找的過程。也就是上面循環流程中的第五步,會嘗試去舊子節點數組中找到與當前新子節點類似的節點,減小dom的操做!
有興趣的能夠看看代碼:
function updateChildren (parentElm, oldCh, newCh) { 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] let oldKeyToIdx, idxInOld, elmToMove, before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // 未定義表示被移動過 } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 頭頭類似 patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾類似 patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // 頭尾類似 patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾頭類似 patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 根據舊子節點的key,生成map映射 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 在舊子節點數組中,找到和newStartVnode類似節點的下標 idxInOld = oldKeyToIdx[newStartVnode.key] if (isUndef(idxInOld)) { // 沒有key,建立並插入dom api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } else { // 有key,找到對應dom ,移動該dom並在oldCh中置爲undefined elmToMove = oldCh[idxInOld] patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] = undefined api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } // 循環結束時,刪除/添加多餘dom if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
但願看完這篇對虛擬dom的比較會有必定的瞭解!若是有什麼錯誤記得悄悄告訴我啊哈哈。
文筆仍是很差,但願你們能理解o(︶︿︶)o
4篇文章寫了兩個月......真是佩服本身的執行力!但發現寫博客好像確實挺費時的(┬_┬),不過之後必定會常常寫,先兩週一篇?😄