面試題——為什麼 Vue 中不要用 index 作為 key?(diff 演算法詳解)

来源:https://www.cnblogs.com/yingzi1028/archive/2022/09/05/16647253.html
-Advertisement-
Play Games

前言 在vue中使用v-for時需要,都會提示或要求使用 :key,有的的開發者會直接使用數組的 index 作為 key 的值,但不建議直接使用 index作為 key 的值,有時我們面試時也會遇到面試官問:為什麼不推薦使用 index 作為 key ?接下來和小穎一起來瞅瞅吧 為什麼要有 key ...


前言

在vue中使用v-for時需要,都會提示或要求使用  :key,有的的開發者會直接使用數組的 index 作為 key 的值,但不建議直接使用 index作為 key 的值,有時我們面試時也會遇到面試官問:為什麼不推薦使用 index 作為 key ?接下來和小穎一起來瞅瞅吧

為什麼要有 key

官網解釋

當 Vue 正在更新使用 v-for 渲染的元素列表時,它預設使用“就地更新”的策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序,而是就地更新每個元素,並且確保它們在每個索引位置正確渲染。這個類似 Vue 1.x 的 track-by="$index"

這個預設的模式是高效的,但是只適用於不依賴子組件狀態或臨時 DOM 狀態 (例如:表單輸入值) 的列表渲染輸出。

為了給 Vue 一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key attribute:

key的作用

官網解釋:key

預期:number | string | boolean (2.4.2 新增) | symbol (2.5.12 新增)

key 的特殊 attribute 主要用在 Vue 的虛擬 DOM 演算法,在新舊 nodes 對比時辨識 VNodes。如果不使用 key,Vue 會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改/復用相同類型元素的演算法。而使用 key 時,它會基於 key 的變化重新排列元素順序,並且會移除 key 不存在的元素。

有相同父元素的子元素必須有獨特的 key。重覆的 key 會造成渲染錯誤。

通俗解釋:

key 在 diff 演算法的作用,就是用來判斷是否是同一個節點。

Vue 中使用虛擬 dom 且根據 diff 演算法進行新舊 DOM 對比,從而更新真實 dom ,key 是虛擬 DOM 對象的唯一標識, 在 diff 演算法中 key 起著極其重要的作用,key可以管理可復用的元素,減少不必要的元素的重新渲染,也要讓必要的元素能夠重新渲染。

為什麼key 值不建議用index?

性能消耗

使用 index 做 key,破壞順序操作的時候, 因為每一個節點都找不到對應的 key,導致部分節點不能復用,所有的新 vnode 都需要重新創建。

示例:

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="index">{{ item.name }}</li>
    </ul>
    <br>
    <button @click="addStudent">添加一條數據</button>
  </div>
</template>

<script>
export default {
  name: 'ceshi',
  data() {
    return {
      studentList: [
        {id: 1, name: '張三', age: 18},
        {id: 2, name: '李四', age: 19},
        {id: 3, name: '王麻子', age: 20},
      ],
    };
  },
  methods: {
    addStudent() {
      const studentObj = {id: 4, name: '王五', age: 20};
      this.studentList = [studentObj, ...this.studentList]
    }
  }
}
</script>

打開瀏覽器的開發工具,修改數據的文本,後面加上 “-我沒變”,點擊添加一條數據   按鈕,則發現dom整體都變了

 

 

 當給key綁定唯一不重覆的值時:

<li v-for="item in studentList" :key="item.id">{{ item.name }}</li>

打開瀏覽器的開發工具,修改數據的文本,後面加上 “-我沒變”,點擊添加一條數據   按鈕,則發現只是頂部多了一條,其他dom沒有重新渲染。

 

 

當用index做key時,當我們在前面加了一條數據時 index 順序就會被打斷,導致新節點 key 全部都改變了,所以導致我們頁面上的數據都被重新渲染了。而用了不會重覆的唯一標識id時,diff演算法比較後發現只有頭部有變化,其他沒有變,則只給頭部新增了一個元素。從上面比較可以看出,用唯一值作為 key 可以節約開銷這樣大家應該就明白了吧·················

參考:在 Vue 中為什麼不推薦用 index 做 key

數據錯位

示例:

<template>
  <div class="hello">
    <ul>
      <li v-for="item in studentList" :key="item.id">{{ item.name }}<input /></li>
    </ul>
    <br>
    <button @click="addStudent">添加一條數據</button>
  </div>
</template>

<script>
export default {
  name: 'ceshi',
  data() {
    return {
      studentList: [
        {id: 1, name: '張三', age: 18},
        {id: 2, name: '李四', age: 19}
      ],
    };
  },
  methods: {
    addStudent() {
      const studentObj = {id: 4, name: '王五', age: 20};
      this.studentList = [studentObj, ...this.studentList]
    }
  }
}
</script>

我們往 input 裡面輸入一些值,添加一位同學看下效果:

 這時候我們就會發現,在添加之前輸入的數據錯位了。添加之後王五的輸入框殘留著張三的信息,這很顯然不是我們想要的結果。

 

 

 從上面比對可以看出來這時因為採用 index 作為 key 時,當在比較時,發現雖然文本值變了,但是當繼續向下比較時發現  DOM 節點還是和原來一摸一樣,就復用了,但是沒想到 input 輸入框殘留輸入的值,這時候就會出現輸入的值出現錯位的情況

當我們將key綁定為唯一標識id時,如圖所示。key 相同的節點都做到了復用。起到了diff 演算法的真正作用。

 

 總結:

  • 用 index 作為 key 時,在對數據進行,逆序添加,逆序刪除等破壞順序的操作時,會產生沒必要的真實 DOM更新,從而導致效率低
  • 用 index 作為 key 時,如果結構中包含輸入類的 DOM,會產生錯誤的 DOM 更新
  • 在開發中最好每條數據使用唯一標識固定的數據作為 key,比如後臺返回的 ID,手機號,身份證號等唯一值
  • 如果不對數據進行逆序添加 逆序刪除破壞順序的操作, 只用於列表展示的話 使用index 作為Key沒有毛病

相關內容

 為什麼要提出虛擬DOM

在Web早期,頁面的交互比較簡單,沒有複雜的狀態需要管理,也不太需要頻繁的操作DOM,隨著時代的發展,頁面上的功能越來越多,我們需要實現的需求也越來越複雜,DOM的操作也越來越頻繁。通過js操作DOM的代價很高,因為會引起頁面的重排重繪,增加瀏覽器的性能開銷,降低頁面渲染速度,既然操作DOM的代價很高那麼有沒有那種方式可以減少對DOM的操作?這就是為什麼提出虛擬DOM一個很重要的原因。 參考: 淺談Vue中的虛擬DOM   虛擬DOM就是為瞭解決瀏覽器性能問題而被設計出來的。若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容保存到本地一個JS對象中,最終將這個JS對象一次性attch到DOM樹上,再進行後續操作,避免大量無謂的計算量。所以,用JS對象模擬DOM節點的好處是,頁面的更新可以先全部反映在JS對象(虛擬DOM)上,操作記憶體中的JS對象的速度顯然要更快,等更新完成後,再將最終的JS對象映射成真實的DOM,交由瀏覽器去繪製。 參考: VUE中key的作用與diff演算法   簡而言之呢          頻繁的操作DOM會影響瀏覽器的性能,為瞭解決這個問題從而提出了 虛擬DOM,虛擬DOM是用javascript對象表示的,而操作javascript是很簡便高效的。虛擬DOM和真正的DOM有一層映射關係,很多需要操作DOM的地方都會去操作虛擬DOM,最後統一一次更新DOM。 因而可以提高性能。

附加面試題:

如果有許多次操作dom的動作,vue中是怎麼更新dom的? vue中不會立即操作dom,而是將要更新的diff內容保存到新的虛擬dom對象中,通過diff演算法後得到最終的虛擬dom,將其映射成真是的dom更新視圖。

虛擬DOM是什麼

      Vue.js通過編譯將模版轉換成渲染函數(render),執行渲染函數就可以得到一個以vnode節點(JavaScript對象)作為基礎的樹形結構,vnode節點裡面包含標簽名(tag)、屬性(attrs)和子元素對象(children)等等屬性,這個樹形結構就是Virtual DOM,簡單來說,可以把Virtual DOM理解為一個樹形結構的JS對象。

參考: 淺談Vue中的虛擬DOM

簡而言之  虛擬DON就是javascript對象,通過對虛擬 DOM進行diff,算出最小差異,然後更新真實的DOM。

虛擬DOM的優勢

  1. 提供一種方便的工具,使得開發效率得到保證
  2. 保證最小化的DOM操作,使得執行效率得到保證
  3. 具備跨平臺的優勢 由於 Virtual DOM 是以 JavaScript 對象為基礎而不依賴真實平臺環境,所以使它具有了跨平臺的能力,比如說瀏覽器平臺、Weex、Node 等。
  4. 提升渲染性能 Virtual DOM的優勢不在於單次的操作,而是在大量、頻繁的數據更新下,能夠對視圖進行合理、高效的更新

參考:vue 中虛擬dom的理解淺談Vue中的虛擬DOM

為什麼虛擬DOM可以提高渲染速度

  1. 根據虛擬dom樹最初渲染成真實dom
  2. 當數據變化,或者說是頁面需要重新渲染的時候,會重新生成一個新的完整的虛擬dom
  3. 拿新的虛擬dom來和舊的虛擬dom做對比(使用diff演算法)。得到需要更新的地方之後,從而只對發生了變化的節點進行更新操作。

如果大家不太明白的話,我們來看個示例

<template>
  <div class="hello">
    <ul>
      <li v-for="(item,index) in studentList" :key="item.id" @click="changeName(index)">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'ceshi',
  data() {
    return {
      studentList: [
        {id: 1, name: '張三', age: 18},
        {id: 2, name: '李四', age: 19},
        {id: 3, name: '王麻子', age: 20},
      ],
    };
  },
  methods: {
    changeName(index) {
      this.studentList[index].name = '我變啦'
    }
  }
}
</script>

上面的修改數組值得方式 僅在 2.2.0+ 版本中支持 Array + index 用法。

如果使用之前的則使用    Vue.set( target, propertyName/index, value )

運行後,我們打開開發者工具,然後手動修改頁面的文本,給每個後面加  “-我沒變” 

 然後隨便點擊其中一個,我們會發現只有點擊那個文本變了,並且只是它的文本內容變了,dom並沒有整體變。

vue中是如何實現模板轉換成視圖的

參考: 淺談Vue中的虛擬DOM

簡單來說就是:通過編譯將模版轉換成渲染函數(render),執行渲染函數就可以得到一個以vnode節點(JavaScript對象)作為基礎的樹形結構(虛擬dom),當數據發生變化虛擬dm通過diff演算法找出新樹和舊樹的不同,記錄兩棵樹差異根據差異應用到所構建的真正的DOM樹上,視圖就更新。

diff演算法

 虛擬DOM中,在DOM的狀態發生變化時,虛擬DOM會進行Diff運算,來更新只需要被替換的DOM,而不是全部重繪。 在Diff演算法中,只平層的比較前後兩棵DOM樹的節點,沒有進行深度的遍歷。

規則

同層比較,如上面div的Old Vnode,跟其Vnode比較,div只會跟同層div比較,不會跟p進行比較,下麵是示例圖:

流程圖

當數據發生改變時,set方法會讓調用Dep.notify通知所有訂閱者Watcher,訂閱者就會調用patch給真實的DOM打補丁,更新相應的視圖。

 

 具體分析

patch

 

 來看看patch是怎麼打補丁的(代碼只保留核心部分)

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el // 當前oldVnode對應的真實元素節點
        let parentEle = api.parentNode(oEl)  // 父元素
        createEle(vnode)  // 根據Vnode生成新元素
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 將新元素添加進父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的舊元素節點
            oldVnode = null
        }
    }
    // some code 
    return vnode
}

patch函數接收兩個參數oldVnodeVnode分別代表新的節點和之前的舊節點

  • 判斷兩節點是否值得比較,值得比較則執行patchVnode
    function sameVnode (a, b) {
      return (
        a.key === b.key &&  // key值
        a.tag === b.tag &&  // 標簽名
        a.isComment === b.isComment &&  // 是否為註釋節點
        // 是否都定義了data,data包含一些具體信息,例如onclick , style
        isDef(a.data) === isDef(b.data) &&  
        sameInputType(a, b) // 當標簽是<input>的時候,type必須相同
      )
    }
  • 不值得比較則用Vnode替換oldVnode

如果兩個節點都是一樣的,那麼就深入檢查他們的子節點。如果兩個節點不一樣那就說明Vnode完全被改變了,就可以直接替換oldVnode

雖然這兩個節點不一樣但是他們的子節點一樣怎麼辦?別忘了,diff可是逐層比較的,如果第一層不一樣那麼就不會繼續深入比較第二層了。(我在想這算是一個缺點嗎?相同子節點不能重覆利用了...)

patchVnode

當我們確定兩個節點值得比較之後我們會對兩個節點指定patchVnode方法。那麼這個方法做了什麼呢?

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

這個函數做了以下事情:

  • 找到對應的真實dom,稱為el
  • 判斷VnodeoldVnode是否指向同一個對象,如果是,那麼直接return
  • 如果他們都有文本節點並且不相等,那麼將el的文本節點設置為Vnode的文本節點。
  • 如果oldVnode有子節點而Vnode沒有,則刪除el的子節點
  • 如果oldVnode沒有子節點而Vnode有,則將Vnode的子節點真實化之後添加到el
  • 如果兩者都有子節點,則執行updateChildren函數比較子節點,這一步很重要

參考:詳解vue的diff演算法 

updataChildren原理 

參考:理解Vue 2.5的Diff演算法 作者:愛喝酸奶的吃貨出處:http://www.cnblogs.com/yingzi1028/ 本博客文章大多為原創,轉載請請在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 開發好APP瞭如何上架apple store市場? 1、進入蘋果的開發者中選項,就會看到以下畫面,點擊紅框內的選項 點擊之後,就會看到以下畫面,如下圖所示選擇相應的選項 之後就會看以下畫面,根據相應的要求填入相應的信息,之後點擊ok。 之後,把證書下載下來 之後打開HBuilder這個軟體,在上方找 ...
  • 1、安裝iOS上架輔助軟體Appuploader 2、申請iOS發佈證書(p12) 3、申請iOS發佈描述文件(mobileprovision) 4、打包ipa 5、上傳ipa到iTunes Connect 6、TestFlight方式安裝到蘋果手機測試 7、設置APP各項信息提交審核 一、下載安裝 ...
  • HMS Core推送服務支持開發者使用HTTPS協議接入Push服務端。Postman是一款介面測試工具,它可以模擬用戶發起的各類HTTP請求,將請求數據發送至服務端,獲取對應的響應結果。Postman可以模擬開發者伺服器申請Access Token,調用鑒權等介面的請求。 因此有很多開發者在測試端 ...
  • What CSS動畫: 就是指元素從一種樣式逐漸過渡為另一種樣式的過程。 實現方式 transition 實現漸變動畫 transform 實現轉變動畫 animation 實現自定義動畫 實現方式 transition 實現漸變動畫 思維導圖 案例 點擊查看代碼 <!DOCTYPE html> < ...
  • 目前h5新增一個文字轉語音的功能(但是正在完善中,勉強能用),h5新增的SpeechSynthesisUtterance實例 首先new一個SpeechSynthesisUtterance對象 使用實例對象的一些屬性,包括: text – 要合成的文字內容,字元串。 lang – 使用的語言,字元串 ...
  • 1.網頁無法選取文字 按下鍵盤的F12調出開發者工具,點擊console控制台,輸入以下代碼後回車即可: 解除網頁無法選取文字 var eles = document.getElementsByTagName('*'); for (var i = 0; i < eles.length; i++) { ...
  • 有沒有發現,在大家使用React/Vue的時候,總離不開一個小尾巴,到哪都得帶著他,那就是react-router/vue-router,而基於它們的第三方框架又出現很多個性化約定和擴展,比如nuxtjs/nextjs/umijs都紛紛推出自己的路由方案。 ...
  • 19 以下代碼執行後,控制臺中的輸出內容為? var a2 = {}, b2 = Symbol('123'), c2 = Symbol('123'); a2[b2] = 'b'; a2[c2] = 'c'; console.log(a2[b2]); 20 以下代碼執行後,控制臺中的輸出內容為? va ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...