徹底理解vue的patch流程和diff算法

前言

上一篇《vue異步更新流程梳理》 梳理了數(shù)據(jù)從賦值到更新到視圖的整體流程采幌。但是最后的步驟vm._update(vm._render()) 只是粗略的提了一嘴嫌术,現(xiàn)在就仔細(xì)的研究它內(nèi)部的細(xì)節(jié)验靡,搞清楚patch流程和diff原理是我們看源碼的重中之重谦纱。

更新過程

  • 代碼
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">{{value}}</div>
  <script src="../dist/vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data() {
        return {
          value: 1
        }
      },
      mounted () {
        this.value = 2
      }
    })
  </script>
</body>
</html>

  • 在vue實(shí)例調(diào)用$mount的時(shí)候,就已經(jīng)把updateComponent 方法通過new Watcher(vm, updateComponent)傳入到渲染watcher里面届氢, 且掛在watcher.getter上独泞,得到一個(gè)渲染watcher, 渲染watcher在以后每次響應(yīng)式數(shù)據(jù)更新都會執(zhí)行watcher.getter 即 updateComponent 方法。


    mount.png
watcher.getter.png

當(dāng)更新數(shù)據(jù)的時(shí)候就會執(zhí)行這個(gè)updateComponent方法信不,即方法里面的vm._update(vm._render())嘲叔,vm.render() 得到一個(gè)vnode,那么vm._update到底干什么? 進(jìn)去看看

image.png

其實(shí)就是判斷是進(jìn)行初始化流程(initial render) 或者是更新流程(updates),但是都是調(diào)用了 vm.__patch__ 函數(shù)抽活;兩者的區(qū)別是

  • 初始化的第一個(gè)參數(shù)是el節(jié)點(diǎn)硫戈,也就是例子中真實(shí)的dom節(jié)點(diǎn)#app
  • 而更新流程的第一個(gè)參數(shù)就是prevVnode, 這個(gè)prevVnode其實(shí)是一個(gè)vnode, 是更新前的vnode

至此,無論是初始化還是更新都是靠patch來完成的 下硕,我們只需要看update流程就可以了丁逝。進(jìn)入patch內(nèi)部

image.png

patch函數(shù)主要接收oldVnode 與 vnode兩個(gè)參數(shù),其實(shí)就是新舊兩棵虛擬樹梭姓。這里經(jīng)過判斷條件 !isRealElement && sameVnode(oldVnode, vnode)霜幼,不是真實(shí)節(jié)點(diǎn) 且是相同的vnode,進(jìn)入patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); 我們只要關(guān)注oldVnode, vnode這兩個(gè)參數(shù)即可誉尖。

按照我們的例子罪既,此時(shí)的oldVnode 與 vnode分別是

oldVnode: {
  tag: 'div',
  key: undefined,
  elm: div#app, // 這是該vnode節(jié)點(diǎn)映射對應(yīng)的真實(shí)dom節(jié)點(diǎn)
  children: [
    { 
      tag: undefined,
      key: undefined,
      elm: text // 文本節(jié)點(diǎn)
      text: '1'
    }
  ]
}

vnode: {
  tag: 'div',
  key: undefined,
  elm: div#app, // 這是該vnode節(jié)點(diǎn)映射對應(yīng)的真實(shí)dom節(jié)點(diǎn)
  children: [
    { 
      tag: '',
      key: undefined,
      elm: undefined, // 注意這里是undefined,此時(shí)還沒有diff
      text: '2'
    }
  ]
}

此處只列出關(guān)鍵屬性tag, key, elm, children,elm铡恕,還有很多其他的屬性沒有列出琢感。真實(shí)的虛擬樹節(jié)點(diǎn)應(yīng)該是如下圖


image.png

我們能看出 兩個(gè)vnode之間就是children[0]的不同:

oldVnode.children[0].text === '1',
vnode.children[0].text === '2'

追蹤流程發(fā)現(xiàn)探熔,我們進(jìn)入oldVnode 與 vnode的children進(jìn)行對比驹针,在updateChildren函數(shù)中。


image.png

我們先不去看updateChildren的邏輯诀艰,繼續(xù)看patchVnode這個(gè)函數(shù)其他的邏輯分支柬甥,得出oldVnode 與 vnode的對比流程

  1. 拿出兩者的children: oldCh, ch
  2. 如果vnode是元素節(jié)點(diǎn)
    2.1,如果oldCh, ch兩者都存在且不相同其垄,進(jìn)入updateChildren流程
    2.2苛蒲,否則如果只是ch 存在,oldCh不存在捉捅,那么直接操作真實(shí)dom, 添加ch節(jié)點(diǎn)
    2.3撤防,否則如果只是oldCh 存在狞膘,ch不存在审轮,那么直接操作真實(shí)dom, 刪除oldCh節(jié)點(diǎn)
    2.4,否則如果只是oldCh是文本袒餐, ch不存在无牵,直接操作真實(shí)dom設(shè)置文本節(jié)點(diǎn)為空 ''
  3. 如果vnode是文本節(jié)點(diǎn)漾肮,直接操作真實(shí)dom, 設(shè)置文本節(jié)點(diǎn)為 vnode.text

總結(jié):patchVnode這個(gè)方法的主要作用是對比兩個(gè)虛擬節(jié)點(diǎn)過程中去更新真實(shí)dom

接下來我們進(jìn)入updateChildren流程,這是兩個(gè)children的對比茎毁,看一下這個(gè)函數(shù)的定義

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            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)
    }
  }

函數(shù)解讀:

  • 參數(shù) parentElm:真實(shí)的dom元素克懊,做為父節(jié)點(diǎn),供更新children時(shí)去插入
  • 參數(shù) oldCh:老的虛擬樹的children
  • 參數(shù) newCh: 新的虛擬樹的children
  • oldStartIdx:老數(shù)組的開始的下標(biāo)index
  • newStartIdx: 新數(shù)組的開始的下標(biāo)index
  • oldEndIdx: 老數(shù)組的結(jié)束的下標(biāo)index
  • oldStartVnode : 老數(shù)組的開始的節(jié)點(diǎn)
  • oldEndVnode : 老數(shù)組的結(jié)束的節(jié)點(diǎn)
  • newEndIdx :新數(shù)組的結(jié)束的下標(biāo)index
  • newStartVnode :新數(shù)組的開始的節(jié)點(diǎn)
  • newEndVnode :新數(shù)組的結(jié)束的節(jié)點(diǎn)
  • oldKeyToIdx:老數(shù)組的節(jié)點(diǎn)的key與index的一個(gè)映射map
  • idxInOld:老數(shù)組的某個(gè)節(jié)點(diǎn)的index
  • vnodeToMove:待移動的節(jié)點(diǎn)七蜘,也就是在oldKeyToIdx中找到key的可復(fù)用的節(jié)點(diǎn)
  • refElm: 一個(gè)真實(shí)的節(jié)點(diǎn)谭溉,用來做參考位置的,表示從哪里開始添加新的dom節(jié)點(diǎn)

下面是兩個(gè)數(shù)組進(jìn)行diff的流程橡卤,也就是diff算法

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }

diff解讀:
新舊兩個(gè)數(shù)組扮念,都有雙端指針,兩端指針向中間靠攏碧库,直到某個(gè)數(shù)組的兩端指針相交則退出循環(huán)柜与。

在這個(gè)過程中,會先判斷是否有以下四種情況

  • 舊首 - 新首 是同一個(gè)節(jié)點(diǎn)嵌灰,互相比較弄匕,深度遞歸進(jìn)行patchVnode流程,
  • 舊尾 - 新尾 是同一個(gè)節(jié)點(diǎn)沽瞭,互相比較迁匠,深度遞歸進(jìn)行patchVnode流程
  • 舊首 - 新尾 是同一個(gè)節(jié)點(diǎn),互相比較驹溃,深度遞歸進(jìn)行patchVnode流程
  • 舊尾 - 新首 是同一個(gè)節(jié)點(diǎn)柒瓣,互相比較,深度遞歸進(jìn)行patchVnode流程

如果不符合這4種情況吠架,那就基于舊數(shù)組遍歷一次芙贫,拿到每個(gè)節(jié)點(diǎn)的key和index,就是oldKeyToIdx: {key1: 0, key2: 1}這種情況。然后去新數(shù)組首個(gè)節(jié)點(diǎn)開始匹配傍药,匹配到就進(jìn)行遞歸patchVnode流程磺平,沒匹配到就進(jìn)行創(chuàng)建新節(jié)點(diǎn),插入到真實(shí)dom節(jié)點(diǎn)里面去拐辽。

當(dāng)循環(huán)結(jié)束拣挪,此時(shí)要么是舊數(shù)組相交,要么是新數(shù)組相交俱诸,只有這兩種情況:

  1. 舊數(shù)組相交菠劝,說明新數(shù)組還沒有相交,那么要根據(jù)相交的位置插入新數(shù)組剩余的未遍歷到節(jié)點(diǎn)
  2. 新數(shù)組相交睁搭,說明舊數(shù)組還沒有相交赶诊,那么要刪除舊數(shù)組剩余的未遍歷到的節(jié)點(diǎn)

至此diff流程結(jié)束笼平。

總結(jié)

兩個(gè)虛擬樹進(jìn)行對比:
patch(oldVnode, vnode) -> patchVnode(oldVnode, vnode) -> updataChildren(oldCh, newCh)
在updataChildren(oldCh, newCh)的過程中也會進(jìn)行 patchVnode(oldVnode, vnode) ,如此虛擬樹深度優(yōu)先遞歸diff完成舔痪。

更加詳細(xì)直觀的圖看此鏈接
https://www.processon.com/view/5e809004e4b08e4e2447d02e

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寓调,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子锄码,更是在濱河造成了極大的恐慌夺英,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滋捶,死亡現(xiàn)場離奇詭異痛悯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)重窟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門灸蟆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人亲族,你說我怎么就攤上這事炒考。” “怎么了霎迫?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵斋枢,是天一觀的道長。 經(jīng)常有香客問我知给,道長瓤帚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任涩赢,我火速辦了婚禮戈次,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筒扒。我一直安慰自己怯邪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布花墩。 她就那樣靜靜地躺著悬秉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冰蘑。 梳的紋絲不亂的頭發(fā)上和泌,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天,我揣著相機(jī)與錄音祠肥,去河邊找鬼武氓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的县恕。 我是一名探鬼主播东羹,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弱睦!你這毒婦竟也來了百姓?” 一聲冷哼從身側(cè)響起渊额,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤况木,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后旬迹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體火惊,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年奔垦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屹耐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椿猎,死狀恐怖惶岭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情犯眠,我是刑警寧澤按灶,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站筐咧,受9級特大地震影響鸯旁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜量蕊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一铺罢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧残炮,春花似錦韭赘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蛋勺,卻和暖如春瓦灶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抱完。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工贼陶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓碉怔,卻偏偏與公主長得像烘贴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子撮胧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容