【vue源碼學習】面試官:爲何在Vue列表組件中要寫key,有什麼做用?

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

前言

在寫Vue.js應用程序時,爲何要在列表組件中寫key?有什麼做用?爲何不推薦使用index做爲key?這幾個問題在面試時,經常被面試官問起,本篇文章將從原理上深刻剖析解惑。前端

本篇文章是該專欄的第二篇文章。往期文章:vue

1、【Vue源碼學習】深刻理解watch的實現原理 —— Watcher的實現 (juejin.cn)node

正文

當數據發生改變時,vue是怎麼更新結點的? —— diff算法詳解

要知道,當咱們修改了某個數據,若是直接渲染到真實dom上會引發整個dom樹的重繪和重排,這樣會形成很大的開銷。那麼有沒有可能不更新所有的DOM,只更新發生改變的DOM呢?這裏就是diff算法的操刀之處!react

先來看一個經典的示例:面試

<ul> 
    <li>1</li> 
    <li>2</li>
</ul>
複製代碼

咱們先根據真實DOM生成一顆virtual DOMvirtual DOM就是將真實的DOM的數據抽取出來,以對象的形式模擬樹形結構)算法

上述代碼的Virtual DOM Tree 大體以下:後端

{ 
    tag: 'ul', 
    children: [ 
                { 
                    tag: 'li', 
                    children: [ { vnode: { text: '1' }}] 
                }, 
                { 
                    tag: 'li', 
                    children: [ { vnode: { text: '2' }}] 
                },
              ] 
}

複製代碼

爲何咱們要使用虛擬dom,而不是直接使用真實的dom呢?api

當數據發生改變時,直接操做真實的dom會引出重繪,在大量修改的狀況下,會拉低性能。而使用虛擬dom會更快,經過批量的內存操做(diff算法),找到發生變化的節點,而後再去操做真實的dom,完成視圖更新,而後把結果輸出到瀏覽器,在大量修改的狀況下性能更優秀,替換效率高。數組

注:VNodeoldVNode都是對象

小思考(歡迎來評論區討論交流)

嚴格來講:

以上所說的好處都只存在於特殊的場景大量修改數據的狀況下

而在正常的操做下,並不會有人閒的沒事去大量修改數據,有時候僅僅須要修改一兩個簡單的dom,卻要去使用虛擬dom走一遍diff算法的邏輯,在這種場景下,使用虛擬dom的性能還會比直接使用真實dom更優秀嗎?

回到示例中:

此時,咱們模擬數據發生修改,即將兩個li中的數據就行交換,交換後的vnode以下:

{
    tag: 'ul',
    children: [{
            tag: 'li',
            children: [{
                vnode: {
                    text: '2'
                }
            }]
        },
        {
            tag: 'li',
            children: [{
                vnode: {
                    text: '1'
                }
            }]
        },
    ]
}
複製代碼

數據修改後會發生什麼?

簡單來說:

virtual DOM某個節點的數據改變後會生成一個新的Vnode,而後VnodeoldVnode做對比,發現有不同的地方就直接修改在真實的DOM上,而後使oldVnode的值爲Vnode

下面來深刻了解這個過程👇👇👇👇:

爲了使後面邏輯更容易理解,這裏先來簡單回顧一下vue的響應式原理過程

data.png

如上圖所示,整個響應式原理的步驟爲:

step1:在vnode階段,數據發生改變

step2:data響應式數據更新

step3:data的改變會通知到觀察者Watche。

step4:觸發了渲染Watcher的回調函數vm._update(vm._render())去驅動試圖更新

圖中的整個過程,若是看過第一篇文章 【Vue源碼學習】深刻理解watch的實現原理 —— Watcher的實現 (juejin.cn),應該能對這個過程有較爲清晰的認識,

其實,在step4中的vm._render()生成的就是vnode,vm._update 會帶着新的 vnode 去走 __patch__ 過程。

下面咱們直接進入 ul 這個 vnode 的 patch 過程(對比新舊節點是否爲相同類型的節點):

1.png

如圖中所示,新舊節點的對比遵循的規則是:同層級比較

經過對新舊vnode的每一層的對應節點,進行下面的比較判斷

  • 節點類型是否相同:
    • 若是類型不相同,直接銷燬舊的vnode,渲染新的vnode
    • 若是類型相同,儘量的作節點的複用(在示例中,tag都是ul,進入👇👇👇)
      • 新vnode是否是文字vnode:
        • 若是是,直接調用瀏覽器的dom api 把節點直接替換掉文字內容便可。
        • 若是不是開始對子節點對比(開始對比示例中的li 👇👇👇)
      • 新舊vnode是否都有children
        • 若是oldVnode沒有,newVnode有:在原來的dom上添加新的子節點
        • 若是oldVnode有,而newVnode沒有:在原來的dom上刪除舊子節點
        • oldVnode和newVnode都有(即都存在li子節點列表,下面進入diff的核心,即新舊節點的diff對比環節👇👇👇)

在講對比過程以前,先來了解源碼中這個過程比較重要的函數:sameVnode:

function sameVnode (a, b) { 
        return ( 
            a.key === b.key &&  
                ( 
                    a.tag === b.tag && 
                    a.isComment === b.isComment &&
                    isDef(a.data) === isDef(b.data) &&
                    sameInputType(a, b) 
                )
        )
  }

複製代碼

它是用來判斷節點是否可複用的關鍵函數。

回到diff的核心對比過程:

初始狀況下,先用四個指針分別指向新舊節點的首尾,而後根據這些指針,在while循環中不停的對新舊節點的兩端進行對比,對比後兩端的指針不斷向內部收縮,直到沒有節點能夠對比爲止。

每一輪的對比過程:

  1. 舊首節點和新首節點用 sameNode 對比。
  2. 舊尾節點和新尾節點用 sameNode 對比
  3. 舊首節點和新尾節點用 sameNode 對比
  4. 舊尾節點和新首節點用 sameNode 對比
  • 若是單個vnode中又有children子列表,那麼就回再去走一遍上面的diff children的過程
  • 若是以上有一項命中,就會遞歸進入patchVnode
  • 若是以上全部邏輯都匹配不到,就會維護一個map表,再將全部舊子節點的key作key。而後再用新vnode的key區找出在就舊節點中能夠複用的位置。

key有什麼做用?爲何要用它?

對整個diff算法邏輯有了大體的瞭解後,再來思考這個問題就有了大體的方向了:

看到上面提到過的sameNode函數:

能夠發現若是傳入的vnode的key不相同的話,就能提早結束掉sameNode函數的邏輯,直接斷定爲false,這樣在必定程度上可以提升新舊vnode對比的效率。另外,若是全部的vnode都有屬於本身惟一標識的key值,那麼在進行新舊vnode對比時,能夠直接維護一個map,將舊節點的key做爲鍵,而後使用新vnode的key去map中查找,從而避免複雜的循環。這也是經典的空間換時間的思想,這樣整個diff過程會更快。

爲何不要使用index做爲key?

既然上面說了,key最主要的做用就是用於做爲vnode的惟一標識,那麼爲何不能使用index做爲key呢?下面從兩個角度來解答:

這裏先模擬一個場景,在一個ul中,經過對數據源中的數組[2,5,3,6]進行v-for循環生成多個li,並用數組的index做爲每個li的key值:

  1. key:0 , value:2
  2. key:1 , value:5
  3. key:2 , value:3
  4. key:3 , value:6

當咱們對數組進行反轉的修改操做時,即數組變爲[6,3,5,2]:

不難想到,此時循環生成的每一個新vnode對應的key,value爲:

  1. key:0 , value:6
  2. key:1 , value:3
  3. key:2 , value:5
  4. key:3 , value:2

矛盾顯而易見,原本按照最合理的邏輯來說,新的第一個vnode徹底能夠直接複用舊的第四個vnode,由於它們應該是同一個vnode,全部的數據也是沒有變化的。

然而上述修改會致使的後果是:當子節點在進行diff的過程當中,舊首節點和新首節點用sameNode對比,這一步的邏輯會命中,從而進行patchVnode操做,檢查props有沒有變動,這裏天然是變動了,因此會經過_props.num = 3去更新這個值,並觸發視圖從新渲染等一系列操做。

這意味着本能夠直接複用的vnode,卻仍是要去進行一系列的從新更新,會產生巨大的性能消耗。而正是由於使用了index做爲key,致使diff的全部優化所有失效。

當咱們對數組進行刪除操做時,刪除後的數組變爲[5,3,6]

則新vnode的key,value對應狀況爲:

  1. 被刪除了
  2. key:0 , value:5
  3. key:1 , value:3
  4. key:2 , value:6

下面來走一邊diff的邏輯,這關鍵函數sameNode看來,它感知不到子組件內部的實現,從上述的sameNode函數中就能看到,sameNode只會只會經過判斷key、 tag是否有data的存在(不關心內部具體的值)是不是註釋節點是不是相同的input type,來判斷是否能夠複用這個節點。

因此此時在diff過程當中,舊的1,2,3和新的2,3,4徹底相同,直接複用,最後舊vnode多出了一個4,就會把舊的4刪掉。這樣的話,原本咱們只是應該把舊的1刪掉,結果把舊的4刪掉了。

因而可知,使用index做爲key,一旦數據發生變化,會給咱們帶來毀滅性的錯誤。

總結

回到問題自己:

  • key有什麼做用?

    key能夠用來作列表組件的惟一標識符,能夠提升diff算法邏輯的效率,主要體如今:一、若是新舊vnodekey值不一樣,sameNode函數會直接斷定爲不可服用。二、有了key,能夠直接維護一個map,將舊子節點的key做爲鍵,再用新vnode的key去map中尋找更新的位子,可以加快查找的過程。

  • 爲何不能用index做爲key? 由於若是循環的數組發生改變,如反轉、刪除操做,新vnode的key依然是按0,1,2的順序排列下來的,會致使vue複用錯誤的節點,走到patchVnode纔會發現,又會去一遍數據更新驅動試圖更新的操做,diff的全部優化都會失效。

結語

本篇文章主要是介紹了diff過程,以及爲何列表組件中要用key? 爲何不能用index做爲key? 這兩個問題的緣由。

本文收錄在vue源碼學習專欄下,本專欄也是基於筆者本身的學習理念:學習要有輸入和輸出。而寫做的,做爲該階段筆者學習vue源碼的輸出,但願一樣能給你答疑解惑。讓讀這篇文章的你有所收穫,既是這次分享最大的意義。感謝你的關注!!!

若是有不清楚的地方或者發現文章的錯誤也歡迎各位在評論區討論和指出。

參考文獻:

但願與你共同進步!!!

相關文章
相關標籤/搜索