用 :key 管理可復用元素

  • 2019 年 11 月 7 日
  • 筆記

input 中的 key

我們先來看一個切換登錄方式的例子:

<div v-if="isUser">      <label>Login with account</label>      <input type="text" placeholder="Enter your account">  </div>  <div v-else>      <label>Login with email</label>      <input type="text" placeholder="Enter your email">  </div>    <button @click="isUser=!isUser">click to toggle</button>

我們會發現,在點擊按鈕切換登錄方式後,輸入框中已有的內容沒有被清除,這是為什麼呢?

引用官方文檔的原話:

Vue 會儘可能高效地渲染元素,通常會復用已有元素而不是從頭開始渲染。

這裡的 input 實際上復用了切換之前的 input。而類似 <input><select><textarea> 這樣的表單元素都有一個 internal state 保存著元素的值,在元素復用時,這個值是會得到保留的。

如果我們希望切換的時候不保留這個值呢?我們可以給兩個 input 添加不同的 key。因為 Vue 是將 key 作為唯一標識從而來識別復用的元素的,如果兩個元素的 key 不同,那麼就相當於告訴 Vue 「這兩個元素是完全獨立的,你不能用其中一個來複用另一個」。

接著再來看一個利用 v-for 生成 input 的例子。 假如我們的程式碼為:

<div id="app">      <div v-for="(item,index) in array">          {{item}}: <input type="text">      </div>  </div>
const app = new Vue({      el:'#app',      data:{          array:["A","B","C","D","E"]      }  })

之後生成的 input 中我們填入字元串作為 internal state。如圖:

在沒有使用 key 的情況下,我們通過 app.array.splice(2,0,"F") 在 BC 之間插入 F,發現:

和之前一樣,因為 Vue 採用的是 就地復用 策略,這意味著 ABCDE 在原地不動的情況下被複用了,CDE 都被重新渲染了一次,但先前的 internal state 仍然保留著。

出於性能考慮,有沒有辦法可以只移動個別元素,單獨渲染要插入的那個新元素呢?有了前面的經驗,我們會想到給每個 input 一個 key 值。

首先我們嘗試將 index 作為 key,之後進行插入操作,發現:

問題依然存在。這是因為,我們將 index 作為復用的判斷依據,相當於告訴 Vue:「只要這兩個東西的 index 一樣,就進行復用」。插入之前 C 的 index 是 2,插入之後 F 的 index 也是 2,於是 F 復用了 C,同理,DE 也被複用了,並因此重新渲染了一次。

index 是會隨著插入刪除改變的值,所以它實際上並不適合作為 key。於是我們想:在進行插入或者刪除操作的時候,有沒有一種值始終不會改變呢?有的,我們可以給每個元素一個單獨的 id。但更簡單的方法是直接使用 item,即元素本身的值,畢竟這個值對每個元素來說也是獨一無二的。

我們將 item 作為 key ,之後進行插入操作,發現:

這回正常了。可以很明顯地看到,每個元素都復用了先前的對應元素,這是因為此時 item (即元素值)才是復用的判斷依據,相當於告訴 Vue:「只要這兩個東西的元素值一樣,就進行復用」。例如對於 C 來說,它只會復用與自己的值一樣的元素,顯然這個元素就是 C 本身。同理,D 復用 D,E 復用 E,CDE 都不需要重新渲染了,只需要後移以方便 F 插入,這時候的性能顯然要好很多。

Virtual DOM 的 Diff 演算法

下面大致從虛擬DOM的Diff演算法實現的角度去解釋一下。

vue 和 react的虛擬 DOM 的 Diff 演算法大致相同,其核心是基於兩個簡單的假設:

  • 兩個相同的組件產生類似的DOM結構,不同的組件產生不同的DOM結構。
  • 同一層級的一組節點,他們可以通過唯一的id進行區分。基於以上這兩點假設,使得虛擬DOM的Diff演算法的複雜度從O(n^3)降到了O(n)。

引用 React』s diff algorithm 中的例子:

當某一層有很多相同的節點時,也就是列表節點時,Diff 演算法的更新過程默認情況下也是遵循以上原則。 比如一下這個情況:

我們希望可以在 B 和 C 之間加一個 F,Diff 演算法默認執行起來是這樣的:

即把 C 更新成 F,D 更新成 C,E 更新成 D,最後再插入 E,這樣顯然很沒有效率。 所以我們需要使用 key 來給每個節點做一個唯一標識,Diff 演算法就可以正確的識別此節點,找到正確的位置區插入新的節點。

所以 key 的作用主要是為了高效的更新虛擬 DOM。

參考: https://stackoverflow.com/questions/44077320/what-is-the-use-of-track-by-or-key-in-v-for-in-vue-js https://juejin.im/post/5aae19aa6fb9a028d4445d1a#comment