寫文章不容易,點個讚唄兄弟 <br> <br> 專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】node
若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧數組
【Vue原理】Diff - 源碼版 之 Diff 流程 app
今天終於要開始探索 Vue 更新DOM 的重點了,就是 Diffdom
Diff 的內容不算多,可是若是要講得很詳細的話,就要說不少了,並且要配不少圖函數
這是 Diff 的最後一篇文章,最重要也是最詳細的一篇了學習
因此本篇內容不少,先提個內容概覽測試
一、分析 Diff 源碼比較步驟 二、我的思考爲何如此比較 三、寫個例子,一步步走個Diff 流程
文章很長,也很是詳細,若是你對這內容有興趣的話,也推薦邊閱讀源碼邊看,若是你對本內容暫時沒有了解,能夠先看不涉及源碼的白話版 Diff - 白話版 spa
下面開始咱們的正文prototype
在以前一篇文章 Diff - 源碼版 之 重新建實例到開始diff ,咱們已經探索了 Vue 是如何重新建實例到開始diff 的3d
你應該還有印象,其中Diff涉及的一個重要函數就是 createPatchFunciton
var patch = createPatchFunction(); Vue.prototype.__patch__ = patch
那麼咱們就來看下這個函數
function createPatchFunction() { return function patch( oldVnode, vnode, parentElm, refElm ) { // 沒有舊節點,直接生成新節點 if (!oldVnode) { createElm(vnode, parentElm, refElm); } else { // 且是同樣 Vnode if (sameVnode(oldVnode, vnode)) { // 比較存在的根節點 patchVnode(oldVnode, vnode); } else { // 替換存在的元素 var oldElm = oldVnode.elm; var _parentElm = oldElm.parentNode // 建立新節點 createElm(vnode, _parentElm, oldElm.nextSibling); // 銷燬舊節點 if (_parentElm) { removeVnodes([oldVnode], 0, 0); } } } return vnode.elm } }
這個函數的做用就是
比較 新節點 和 舊節點 有什麼不一樣,而後完成更新
因此你看到接收一個 oldVnode 和 vnode
處理的流程分爲
一、沒有舊節點 二、舊節點 和 新節點 自身同樣(不包括其子節點) 三、舊節點 和 新節點自身不同
速度來看下這三個流程了
沒有舊節點,說明是頁面剛開始初始化的時候,此時,根本不須要比較了
直接所有都是新建,因此只調用 createElm
經過 sameVnode 判斷節點是否同樣,這個函數在上篇文章中說過了
舊節點 和 新節點自身同樣時,直接調用 patchVnode 去處理這兩個節點
patchVnode 下面會講到這個函數
在講 patchVnode 以前,咱們先思考這個函數的做用是什麼?
當兩個Vnode自身同樣的時候,咱們須要作什麼?
首先,自身同樣,咱們能夠先簡單理解,是 Vnode 的兩個屬性 tag 和 key 同樣
那麼,咱們是不知道其子節點是否同樣的,因此確定須要比較子節點
因此,patchVnode 其中的一個做用,就是比較子節點
當兩個節點不同的時候,不難理解,直接建立新節點,刪除舊節點
在上一個函數 createPatchFunction 中,有出現一個函數 patchVnode
咱們思考了這個函數的其中的一個做用是 比較兩個Vnode 的子節點
是否是咱們想的呢,能夠先來過一下源碼
function patchVnode(oldVnode, vnode) { if (oldVnode === vnode) return var elm = vnode.elm = oldVnode.elm; var oldCh = oldVnode.children; var ch = vnode.children; // 更新children if (!vnode.text) { // 存在 oldCh 和 ch 時 if (oldCh && ch) { if (oldCh !== ch) updateChildren(elm, oldCh, ch); } // 存在 newCh 時,oldCh 只能是不存在,若是存在,就跳到上面的條件了 else if (ch) { if (oldVnode.text) elm.textContent = ''; for (var i = 0; i <= ch.length - 1; ++i) { createElm( ch[i],elm, null ); } } else if (oldCh) { for (var i = 0; i<= oldCh.length - 1; ++i) { oldCh[i].parentNode.removeChild(el); } } else if (oldVnode.text) { elm.textContent = ''; } } else if (oldVnode.text !== vnode.text) { elm.textContent = vnode.text; } }
咱們如今就來分析這個函數
沒錯,正如咱們所想,這個函數的確會去比較處理子節點
總的來講,這個函數的做用是
一、Vnode 是文本節點,則更新文本(文本節點不存在子節點)
二、Vnode 有子節點,則處理比較更新子節點
更進一步的總結就是,這個函數主要作了兩種判斷的處理
一、Vnode 是不是文本節點
二、Vnode 是否有子節點
下面咱們來看看這些步驟的詳細分析
當 VNode 存在 text 這個屬性的時候,就證實了 Vnode 是文本節點
咱們能夠先來看看 文本類型的 Vnode 是什麼樣子
因此當 Vnode 是文本節點的時候,須要作的就是,更新文本
一樣有兩種處理
一、當 新Vnode.text 存在,並且和 舊 VNode.text 不同時
直接更新這個 DOM 的 文本內容
elm.textContent = vnode.text;
注:textContent 是 真實DOM 的一個屬性, 保存的是 dom 的文本,因此直接更新這個屬性
二、新Vnode 的 text 爲空,直接把 文本DOM 賦值給空
elm.textContent = '';
當 Vnode 存在子節點的時候,由於不知道 新舊節點的子節點是否同樣,因此須要比較,才能完成更新
這裏有三種處理
一、新舊節點 都有子節點,並且不同
二、只有新節點
三、只有舊節點
後面兩個節點,相信你們都能想通,可是咱們仍是說一下
只有新節點,不存在舊節點,那麼沒得比較了,全部節點都是全新的
因此直接所有新建就行了,新建是指建立出全部新DOM,而且添加進父節點的
只有舊節點而沒有新節點,說明更新後的頁面,舊節點所有都不見了
那麼要作的,就是把全部的舊節點刪除
也就是直接把DOM 刪除
咦惹,又出現了一個新函數,那就是 updateChildren
預告一下,這個函數很是的重要,是 Diff 的核心模塊,蘊含着 Diff 的思想
可能會有點繞,可是不用怕,相信在個人探索之下,能夠稍微明白些
一樣的,咱們先來思考下 updateChildren 的做用
記得條件,當新節點 和 舊節點 都存在,要怎麼去比較才能知道有什麼不同呢?
哦沒錯,使用遍歷,新子節點和舊子節點一個個比較
若是同樣,就不更新,若是不同,就更新
下面就來驗證下咱們的想法,來探索一下 updateChildren 的源碼
這個函數很是的長,可是其實不難,就是分了幾種處理流程而已,可是一開始看可能有點懵
或者能夠先跳過源碼,看下分析,或者便看分析邊看源碼
function updateChildren(parentElm, oldCh, newCh) { var oldStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newStartIdx = 0; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, vnodeToMove, refElm; // 不斷地更新 OldIndex 和 OldVnode ,newIndex 和 newVnode while ( oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx ) { if (!oldStartVnode) { oldStartVnode = oldCh[++oldStartIdx]; } else if (!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); // oldStartVnode 放到 oldEndVnode 後面,還要找到 oldEndValue 後面的節點 parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } // 舊尾 和新頭 比較 else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode); // oldEndVnode 放到 oldStartVnode 前面 parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } // 單個新子節點 在 舊子節點數組中 查找位置 else { // oldKeyToIdx 是一個 把 Vnode 的 key 和 index 轉換的 map if (!oldKeyToIdx) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } // 使用 newStartVnode 去 OldMap 中尋找 相同節點,默認key存在 idxInOld = oldKeyToIdx[newStartVnode.key] // 新孩子中,存在一個新節點,老節點中沒有,須要新建 if (!idxInOld) { // 把 newStartVnode 插入 oldStartVnode 的前面 createElm( newStartVnode, parentElm, oldStartVnode.elm ); } else { // 找到 oldCh 中 和 newStartVnode 同樣的節點 vnodeToMove = oldCh[idxInOld]; if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode); // 刪除這個 index oldCh[idxInOld] = undefined; // 把 vnodeToMove 移動到 oldStartVnode 前面 parentElm.insertBefore( vnodeToMove.elm, oldStartVnode.elm ); } // 只能建立一個新節點插入到 parentElm 的子節點中 else { // same key but different element. treat as new element createElm( newStartVnode, parentElm, oldStartVnode.elm ); } } // 這個新子節點更新完畢,更新 newStartIdx,開始比較下一個 newStartVnode = newCh[++newStartIdx]; } } // 處理剩下的節點 if (oldStartIdx > oldEndIdx) { var newEnd = newCh[newEndIdx + 1] refElm = newEnd ? newEnd.elm :null; for (; newStartIdx <= newEndIdx; ++newStartIdx) { createElm( newCh[newStartIdx], parentElm, refElm ); } } // 說明新節點比對完了,老節點可能還有,須要刪除剩餘的老節點 else if (newStartIdx > newEndIdx) { for (; oldStartIdx<=oldEndIdx; ++oldStartIdx) { oldCh[oldStartIdx].parentNode.removeChild(el); } } }
處理的是 新子節點 和 舊子節點,循環遍歷逐個比較
一、使用 while
二、新舊節點數組都配置首尾兩個索引
新節點的兩個索引:newStartIdx , newEndIdx
舊節點的兩個索引:oldStartIdx,oldEndIdx
以兩邊向中間包圍的形式 來進行遍歷
頭部的子節點比較完畢,startIdx 就加1
尾部的子節點比較完畢,endIdex 就減1
只要其中一個數組遍歷完(startIdx<endIdx),則結束遍歷
源碼處理的流程分爲兩個
一、比較新舊子節點
二、比較完畢,處理剩下的節點
咱們來逐個說明這兩個流程
注:這裏有兩個數組,一個是 新子Vnode數組,一箇舊子Vnode數組
在比較過程當中,不會對兩個數組進行改變(好比不會插入,不會刪除其子項)
而全部比較過程當中都是直接 插入刪除 真實頁面DOM
找到 新舊子節點中 的 相同的子節點,儘可能以 移動 替代 新建 去更新DOM
只有在實在不一樣的狀況下,纔會新建
首先考慮,不移動DOM
其次考慮,移動DOM
最後考慮,新建 / 刪除 DOM
能不移動,儘可能不移動。不行就移動,實在不行就新建
下面開始說源碼中的比較邏輯
五種比較邏輯以下
一、舊頭 == 新頭 二、舊尾 == 新尾 三、舊頭 == 新尾 四、舊尾 == 新頭 五、單個查找
來分析下這五種比較邏輯
sameVnode(oldStartVnode, newStartVnode)
當兩個新舊的兩個頭同樣的時候,並不用作什麼處理
符合咱們的步驟第一條,不移動DOM完成更新
可是看到一句,patchVnode
就是爲了繼續處理這兩個相同節點的子節點,或者更新文本
由於咱們不考慮多層DOM 結構,因此 新舊兩個頭同樣的話,這裏就算結束了
能夠直接進行下一輪循環
newStartIdx ++ , oldStartIdx ++
sameVnode(oldEndVnode, newEndVnode)
和 頭頭 相同的處理是同樣的
尾尾相同,直接跳入下個循環
newEndIdx ++ , oldEndIdx ++
sameVnode(oldStartVnode, newEndVnode)
這步不符合 不移動DOM,因此只能 移動DOM 了
源碼是這樣的
parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling );
以 新子節點的位置 來移動的,舊頭 在新子節點的 末尾
因此把 oldStartVnode 的 dom 放到 oldEndVnode 的後面
可是由於沒有把dom 放到誰後面的方法,因此只能使用 insertBefore
即放在 oldEndVnode 後一個節點的前面
圖示是這樣的
而後更新兩個索引
oldStartIdx++,newEndIdx--
sameVnode(oldEndVnode, newStartVnode)
一樣不符合 不移動DOM,也只能 移動DOM 了
parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm );
把 oldEndVnode DOM 直接放到 當前 oldStartVnode.elm 的前面
圖示是這樣的
而後更新兩個索引
oldEndIdx--,newStartIdx++
當前面四種比較邏輯都不行的時候,這是最後一種處理方法
拿 新子節點的子項,直接去 舊子節點數組中遍歷,找同樣的節點出來
流程大概是
一、生成舊子節點數組以 vnode.key 爲key 的 map 表
二、拿到新子節點數組中 一個子項,判斷它的key是否在上面的map 中
三、不存在,則新建DOM
四、存在,繼續判斷是否 sameVnode
下面就詳細說一下
這個map 表的做用,就主要是判斷存在什麼舊子節點
好比你的舊子節點數組是
[{ tag:"div", key:1 },{ tag:"strong", key:2 },{ tag:"span", key:4 }]
通過 createKeyToOldIdx 生成一個 map 表 oldKeyToIdx
{ vnodeKey: 數組Index }
屬性名是 vnode.key,屬性值是 該 vnode 在children 的位置
是這樣(具體源碼看上篇文章 Diff - 源碼版 之 相關輔助函數)
oldKeyToIdx = { 1:0, 2:1, 4:2 }
拿到新子節點中的 子項Vnode,而後拿到它的 key
去匹配map 表,判斷是否有相同節點
oldKeyToIdx[newStartVnode.key]
直接建立DOM,並插入oldStartVnode 前面
createElm(newStartVnode, parentElm, oldStartVnode.elm);
找到這個舊子節點,而後判斷和新子節點是否 sameVnode
若是相同,直接移動到 oldStartVnode 前面
若是不一樣,直接建立插入 oldStartVnode 前面
咱們上面說了比較子節點的處理的流程分爲兩個
一、比較新舊子節點
二、比較完畢,處理剩下的節點
比較新舊子節點上面已經說完了,下面就到了另外一個流程,比較剩餘的節點,詳情看下面
在updateChildren 中,比較完新舊兩個數組以後,可能某個數組會剩下部分節點沒有被處理過,因此這裏須要統一處理
newStartIdx > newEndIdx
新子節點遍歷完畢,舊子節點可能還有剩
因此咱們要對可能剩下的舊節點進行 批量刪除!
就是遍歷剩下的節點,逐個刪除DOM
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) { oldCh[oldStartIdx] .parentNode .removeChild(el); }
oldStartIdx > oldEndIdx
舊子節點遍歷完畢,新子節點可能有剩
因此要對剩餘的新子節點處理
很明顯,剩餘的新子節點不存在 舊子節點中,因此所有新建
for (; newStartIdx <= newEndIdx; ++newStartIdx) { createElm( newCh[newStartIdx], parentElm, refElm ); }
可是新建有一個問題,就是插在哪裏?
因此其中的 refElm 就成了疑點,看下源碼
var newEnd = newCh[newEndIdx + 1] refElm = newEnd ? newEnd.elm :null;
refElm 獲取的是 newEndIdx 後一位的節點
當前沒有處理的節點是 newEndIdx
也就是說 newEndIdx+1 的節點若是存在的話,確定被處理過了
若是 newEndIdx 沒有移動過,一直是最後一位,那麼就不存在 newCh[newEndIdx + 1]
那麼 refElm 就是空,那麼剩餘的新節點 就所有添加進 父節點孩子的末尾,至關於
for (; newStartIdx <= newEndIdx; ++newStartIdx) { parentElm.appendChild( newCh[newStartIdx] ); }
若是 newEndIdx 移動過,那麼就逐個添加在 refElm 的前面,至關於
for (; newStartIdx <= newEndIdx; ++newStartIdx) { parentElm.insertBefore( newCh[newStartIdx] , refElm ); }
如圖
咱們已經講完了全部 Diff 的內容,你們也應該能領悟到 Diff 的思想
可是我強迫本身去思考一個問題,就是
如下純屬我的意淫想法,沒有權威認證,僅供參考
咱們全部的比較,都是爲了找到 新子節點 和 舊子節點 同樣的子節點
並且咱們的比較處理的宗旨是
一、能不移動,儘可能不移動
二、沒得辦法,只好移動
三、實在不行,新建或刪除
首先,一開始比較,確定是按照咱們的第一宗旨 不移動 ,找到能夠不移動的節點
而 頭頭,尾尾比較 符合咱們的第一宗旨,因此出如今最開始,嗯,這個能夠想通
而後就到咱們的第二宗旨 移動,按照 updateChildren 的作法有
舊頭新尾比較,舊尾新頭比較,單個查找比較
我開始疑惑了,咦?頭尾比較爲了移動我知道,可是爲何要出現這種比較?
明明我能夠用 單個查找 的方式,完成全部的移動操做啊?
我思考了好久,頭和尾的關係,以爲多是爲了不極端狀況的消耗??
好比當咱們去掉頭尾比較,所有使用單個查找的方式
若是出現頭 和 尾 節點同樣的時候,一個節點須要遍歷 從頭找到尾 才能找到相同節點
這樣實在是太消耗了,因此這裏加入了 頭尾比較 就是爲了排除 極端狀況形成的消耗操做
固然,這只是我我的的想法,僅供參考,雖然這麼說,我也的確作了個例子測試
子節點中加入了出現兩個頭尾比較狀況的子項 b div
oldCh = ['header','span','div','b'] newCh = ['sub','b','div','strong']
使用 Vue 去更新,比較更新速度,而後更新十次,計算平均值
一、全用 單個查找,用時 0.91ms
二、加入頭尾比較,用時 0.853ms
的確是快一些喔
我相信通過這麼長的一篇文章,你們的腦海中尚未把全部的知識點集合起來,可能對整個流程還有點模糊
沒事,咱們如今就來舉一個例子,一步步走流程,完成更新
如下的節點,綠色表示未處理,灰色表示已經處理,淡綠色表示正在處理,紅色表示新插入,以下
如今Vue 須要更新,存在下面兩組新舊子節點,須要進行比較,來判斷須要更新哪些節點
更新索引,newStartIdx++ , oldStartIdx++
開始下輪處理
更新索引,newEndIdx-- ,oldStartIdx ++
開始下輪處理
更新索引,oldEndIdx-- ,newStartIdx++
開始下輪比較
找不到同樣的,直接建立插入到 oldStartVnode 前面
更新索引,newStartIdx++
此時 newStartIdx> newEndIdx ,結束循環
此時看 舊 Vnode 數組中, oldStartIdx 和 oldEndIdx 都指向同一個節點,因此只用刪除 oldVnode-4 這個節點
ok,完成全部比較流程
耶,Diff 內容講完了,謝謝你們的觀看
鑑於本人能力有限,不免會有疏漏錯誤的地方,請你們多多包涵,若是有任何描述不當的地方,歡迎去後臺去聯繫本人,有重謝