电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

詳解為什么Vue中不要用index作為key(diff算法)

瀏覽:142日期:2023-01-28 16:13:39

前言

Vue 中的 key 是用來做什么的?為什么不推薦使用 index 作為 key?常常聽說這樣的問題,本篇文章帶你從原理來一探究竟。另外本文的結論對于性能的毀滅是針對列表子元素順序會交換、或者子元素被刪除的特殊情況,提前說明清楚,噴子繞道。

本篇已經收錄在 Github 倉庫,歡迎 Star:https://github.com/sl1673495/blogs/issues/39

示例

以這樣一個列表為例:

<ul> <li>1</li> <li>2</li></ul>

那么它的 vnode 也就是虛擬 dom 節(jié)點大概是這樣的。

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

假設更新以后,我們把子節(jié)點的順序調換了一下:

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

很顯然,這里的 children 部分是我們本文 diff 算法要講的重點(敲黑板)。

首先響應式數據更新后,觸發(fā)了 渲染 Watcher 的回調函數 vm._update(vm._render())去驅動視圖更新,vm._render() 其實生成的就是 vnode,而 vm._update 就會帶著新的 vnode 去走觸發(fā) __patch__ 過程。

我們直接進入 ul 這個 vnode 的 patch 過程。

對比新舊節(jié)點是否是相同類型的節(jié)點:

1. 不是相同節(jié)點:isSameNode為false的話,直接銷毀舊的 vnode,渲染新的 vnode。這也解釋了為什么 diff 是同層對比。

2. 是相同節(jié)點,要盡可能的做節(jié)點的復用(都是 ul,進入👈)。

會調用src/core/vdom/patch.js下的patchVNode方法。

如果新 vnode 是文字 vnode

就直接調用瀏覽器的 dom api 把節(jié)點的直接替換掉文字內容就好。

如果新 vnode 不是文字 vnode如果有新 children 而沒有舊 children

說明是新增 children,直接 addVnodes 添加新子節(jié)點。

如果有舊 children 而沒有新 children

說明是刪除 children,直接 removeVnodes 刪除舊子節(jié)點

如果新舊 children 都存在(都存在 li 子節(jié)點列表,進入👈)

那么就是我們 diff算法 想要考察的最核心的點了,也就是新舊節(jié)點的 diff 過程。

通過

// 舊首節(jié)點 let oldStartIdx = 0 // 新首節(jié)點 let newStartIdx = 0 // 舊尾節(jié)點 let oldEndIdx = oldCh.length - 1 // 新尾節(jié)點 let newEndIdx = newCh.length - 1

這些變量分別指向舊節(jié)點的首尾、新節(jié)點的首尾。

根據這些指針,在一個 while 循環(huán)中不停的對新舊節(jié)點的兩端的進行對比,直到沒有節(jié)點可以對比。

在講對比過程之前,要講一個比較重要的函數: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) ) ) )}

它是用來判斷節(jié)點是否可用的關鍵函數,可以看到,判斷是否是 sameVnode,傳遞給節(jié)點的 key 是關鍵。然后我們接著進入 diff 過程,每一輪都是同樣的對比,其中某一項命中了,就遞歸的進入 patchVnode 針對單個 vnode 進行的過程(如果這個 vnode 又有 children,那么還會來到這個 diff children 的過程 ):

舊首節(jié)點和新首節(jié)點用 sameNode 對比。 舊尾節(jié)點和新首節(jié)點用 sameNode 對比 舊首節(jié)點和新尾節(jié)點用 sameNode 對比 舊尾節(jié)點和新尾節(jié)點用 sameNode 對比 如果以上邏輯都匹配不到,再把所有舊子節(jié)點的 key 做一個映射表,然后用新 vnode 的 key 去找出在舊節(jié)點中可以復用的位置。

然后不停的把匹配到的指針向內部收縮,直到新舊節(jié)點有一端的指針相遇(說明這個端的節(jié)點都被patch過了)。在指針相遇以后,還有兩種比較特殊的情況:

有新節(jié)點需要加入。如果更新完以后,oldStartIdx > oldEndIdx,說明舊節(jié)點都被 patch 完了,但是有可能還有新的節(jié)點沒有被處理到。接著會去判斷是否要新增子節(jié)點。 有舊節(jié)點需要刪除。如果新節(jié)點先patch完了,那么此時會走 newStartIdx > newEndIdx 的邏輯,那么就會去刪除多余的舊子節(jié)點。

為什么不要以index作為key?

節(jié)點reverse場景

假設我們有這樣的一段代碼:

<div id='app'> <ul> <item :key='index' v-for='(num, index) in nums' :num='num' : ></item> </ul> <button @click='change'>改變</button> </div> <script src='http://www.hdgsjgj.cn/bcjs/vue.js'></script> <script> var vm = new Vue({ name: 'parent', el: '#app', data: { nums: [1, 2, 3] }, methods: { change() { this.nums.reverse(); } }, components: { item: { props: ['num'], template: ` <div> {{num}} </div>`, name: 'child' } } }); </script>

其實是一個很簡單的列表組件,渲染出來 1 2 3 三個數字。我們先以 index 作為key,來跟蹤一下它的更新。

我們接下來只關注 item 列表節(jié)點的更新,在首次渲染的時候,我們的虛擬節(jié)點列表 oldChildren 粗略表示是這樣的:

[ { tag: 'item', key: 0, props: { num: 1 } }, { tag: 'item', key: 1, props: { num: 2 } }, { tag: 'item', key: 2, props: { num: 3 } }];

在我們點擊按鈕的時候,會對數組做 reverse 的操作。那么我們此時生成的 newChildren 列表是這樣的:

[ { tag: 'item', key: 0, props: {+ num: 3 } }, { tag: 'item', key: 1, props: {+ num: 2 } }, { tag: 'item', key: 2, props: {+ num: 1 } }];

發(fā)現(xiàn)什么問題沒有?key的順序沒變,傳入的值完全變了。這會導致一個什么問題?

本來按照最合理的邏輯來說,舊的第一個vnode 是應該直接完全復用 新的第三個vnode的,因為它們本來就應該是同一個vnode,自然所有的屬性都是相同的。

但是在進行子節(jié)點的 diff 過程中,會在 舊首節(jié)點和新首節(jié)點用sameNode對比。 這一步命中邏輯,因為現(xiàn)在新舊兩次首部節(jié)點 的 key 都是 0了,

然后把舊的節(jié)點中的第一個 vnode 和 新的節(jié)點中的第一個 vnode 進行 patchVnode 操作。

這會發(fā)生什么呢?我可以大致給你列一下:

首先,正如我之前的文章props的更新如何觸發(fā)重渲染?里所說,在進行 patchVnode 的時候,會去檢查 props 有沒有變更,如果有的話,會通過 _props.num = 3 這樣的邏輯去更新這個響應式的值,觸發(fā) dep.notify,觸發(fā)子組件視圖的重新渲染等一套很重的邏輯。

然后,還會額外的觸發(fā)以下幾個鉤子,假設我們的組件上定義了一些dom的屬性或者類名、樣式、指令,那么都會被全量的更新。

updateAttrs updateClass updateDOMListeners updateDOMProps updateStyle updateDirectives

而這些所有重量級的操作(虛擬dom發(fā)明的其中一個目的不就是為了減少真實dom的操作么?),都可以通過直接復用 第三個vnode 來避免,是因為我們偷懶寫了 index 作為 key,而導致所有的優(yōu)化失效了。

節(jié)點刪除場景

另外,除了會導致性能損耗以外,在刪除子節(jié)點的場景下還會造成更嚴重的錯誤,

可以看sea_ljf同學提供的這個demo。

假設我們有這樣的一段代碼:

<body> <div id='app'> <ul> <li v-for='(value, index) in arr' :key='index'> <test /> </li> </ul> <button @click='handleDelete'>delete</button> </div> </div></body><script> new Vue({ name: 'App', el: ’#app’, data() { return { arr: [1, 2, 3] }; }, methods: { handleDelete() { this.arr.splice(0, 1); } }, components: { test: { template: '<li>{{Math.random()}}</li>' } } })</script>

那么一開始的 vnode列表是:

[ { tag: 'li', key: 0, // 這里其實子組件對應的是第一個 假設子組件的text是1 }, { tag: 'li', key: 1, // 這里其實子組件對應的是第二個 假設子組件的text是2 }, { tag: 'li', key: 2, // 這里其實子組件對應的是第三個 假設子組件的text是3 }];

有一個細節(jié)需要注意,正如我上一篇文章中所提到的為什么說 Vue 的響應式更新比 React 快?,Vue 對于組件的 diff 是不關心子組件內部實現(xiàn)的,它只會看你在模板上聲明的傳遞給子組件的一些屬性是否有更新。

也就是和v-for平級的那部分,回顧一下判斷 sameNode 的時候,只會判斷key、 tag、是否有data的存在(不關心內部具體的值)、是否是注釋節(jié)點、是否是相同的input type,來判斷是否可以復用這個節(jié)點。

<li v-for='(value, index) in arr' :key='index'> // 這里聲明的屬性 <test /></li>

有了這些前置知識以后,我們來看看,點擊刪除子元素后,vnode 列表 變成什么樣了。

[ // 第一個被刪了 { tag: 'li', key: 0, // 這里其實上一輪子組件對應的是第二個 假設子組件的text是2 }, { tag: 'li', key: 1, // 這里其實子組件對應的是第三個 假設子組件的text是3 },];

雖然在注釋里我們自己清楚的知道,第一個 vnode 被刪除了,但是對于 Vue 來說,它是感知不到子組件里面到底是什么樣的實現(xiàn)(它不會深入子組件去對比文本內容),那么這時候 Vue 會怎么 patch 呢?

由于對應的 key使用了 index導致的錯亂,它會把

原來的第一個節(jié)點text: 1直接復用。 原來的第二個節(jié)點text: 2直接復用。 然后發(fā)現(xiàn)新節(jié)點里少了一個,直接把多出來的第三個節(jié)點text: 3 丟掉。

至此為止,我們本應該把 text: 1節(jié)點刪掉,然后text: 2、text: 3 節(jié)點復用,就變成了錯誤的把 text: 3 節(jié)點給刪掉了。

為什么不要用隨機數作為key?

<item :key='Math.random()' v-for='(num, index) in nums' :num='num' : />

其實我聽過一種說法,既然官方要求一個 唯一的key,是不是可以用 Math.random() 作為 key 來偷懶?這是一個很雞賊的想法,看看會發(fā)生什么吧。

首先 oldVnode 是這樣的:

[ { tag: 'item', key: 0.6330715699108844, props: { num: 1 } }, { tag: 'item', key: 0.25104533240710514, props: { num: 2 } }, { tag: 'item', key: 0.4114769152411637, props: { num: 3 } }];

更新以后是:

[ { tag: 'item',+ key: 0.11046018699748683, props: {+ num: 3 } }, { tag: 'item',+ key: 0.8549799545696619, props: {+ num: 2 } }, { tag: 'item',+ key: 0.18674467938937478, props: {+ num: 1 } }];

可以看到,key 變成了完全全新的 3 個隨機數。

上面說到,diff 子節(jié)點的首尾對比如果都沒有命中,就會進入 key 的詳細對比過程,簡單來說,就是利用舊節(jié)點的 key -> index 的關系建立一個 map 映射表,然后用新節(jié)點的 key 去匹配,如果沒找到的話,就會調用 createElm 方法 重新建立 一個新節(jié)點。

具體代碼在這:

// 建立舊節(jié)點的 key -> index 映射表oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);// 去映射表里找可以復用的 indexidxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);// 一定是找不到的,因為新節(jié)點的 key 是隨機生成的。if (isUndef(idxInOld)) { // 完全通過 vnode 新建一個真實的子節(jié)點 createElm();}

也就是說,咱們的這個更新過程可以這樣描述:

123 -> 前面重新創(chuàng)建三個子組件 -> 321123 -> 刪除、銷毀后面三個子組件 -> 321。發(fā)現(xiàn)問題了吧?這是毀滅性的災難,創(chuàng)建新的組件和銷毀組件的成本你們曉得的伐……本來僅僅是對組件移動位置就可以完成的更新,被我們毀成這樣了。

總結

經過這樣的一段旅行,diff 這個龐大的過程就結束了。

我們收獲了什么?

用組件唯一的 id(一般由后端返回)作為它的 key,實在沒有的情況下,可以在獲取到列表的時候通過某種規(guī)則為它們創(chuàng)建一個 key,并保證這個 key 在組件整個生命周期中都保持穩(wěn)定。 如果你的列表順序會改變,別用 index 作為 key,和沒寫基本上沒區(qū)別,因為不管你數組的順序怎么顛倒,index 都是 0, 1, 2 這樣排列,導致 Vue 會復用錯誤的舊子節(jié)點,做很多額外的工作。列表順序不變也盡量別用,可能會誤導新人。 千萬別用隨機數作為 key,不然舊節(jié)點會被全部刪掉,新節(jié)點重新創(chuàng)建,你的老板會被你氣死。

到此這篇關于詳解為什么Vue中不要用index作為key(diff算法)的文章就介紹到這了,更多相關Vue不要用index作為key內容請搜索好吧啦網以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Vue
相關文章:
主站蜘蛛池模板: LZ-373测厚仪-华瑞VOC气体检测仪-个人有毒气体检测仪-厂家-深圳市深博瑞仪器仪表有限公司 | 微型实验室真空泵-无油干式真空泵-微型涡旋耐腐蚀压缩机-思科涡旋科技(杭州)有限公司 | 无线讲解器-导游讲解器-自助讲解器-分区讲解系统 品牌生产厂家[鹰米讲解-合肥市徽马信息科技有限公司] | 高低温万能试验机_拉力试验机_拉伸试验机-馥勒仪器科技(上海)有限公司 | PC构件-PC预制构件-构件设计-建筑预制构件-PC构件厂-锦萧新材料科技(浙江)股份有限公司 | 黄石妇科医院_黄石东方女子医院_黄石东方妇产医院怎么样 | 杭州中央空调维修_冷却塔/新风机柜/热水器/锅炉除垢清洗_除垢剂_风机盘管_冷凝器清洗-杭州亿诺能源有限公司 | 臻知网大型互动问答社区-你的问题将在这里得到解答!-无锡据风网络科技有限公司 | 双舌接地线-PC68数字式高阻计-ZC36|苏海百科| 家庭教育吧-在线家庭教育平台,专注青少年家庭教育 | 北京印刷厂_北京印刷_北京印刷公司_北京印刷厂家_北京东爵盛世印刷有限公司 | 密集架|电动密集架|移动密集架|黑龙江档案密集架-大量现货厂家销售 | 瓶盖扭矩仪(扭力值检测)-百科 | 首页-浙江橙树网络技术有限公司| 赛默飞Thermo veritiproPCR仪|ProFlex3 x 32PCR系统|Countess3细胞计数仪|371|3111二氧化碳培养箱|Mirco17R|Mirco21R离心机|仟诺生物 | app开发|app开发公司|小程序开发|物联网开发||北京网站制作|--前潮网络 | 硅胶制品-硅橡胶制品-东莞硅胶制品厂家-广东帝博科技有限公司 | 清洁设备_洗地机/扫地机厂家_全自动洗地机_橙犀清洁设备官网 | 健康管理师报考条件,考试时间,报名入口—首页 | 双齿辊破碎机-大型狼牙破碎机视频-对辊破碎机价格/型号图片-金联机械设备生产厂家 | 玉米深加工设备|玉米加工机械|玉米加工设备|玉米深加工机械-河南成立粮油机械有限公司 | 菲希尔X射线测厚仪-菲希尔库伦法测厚仪-无锡骏展仪器有限责任公司 | 高空重型升降平台_高空液压举升平台_高空作业平台_移动式升降机-河南华鹰机械设备有限公司 | 国产离子色谱仪,红外分光测油仪,自动烟尘烟气测试仪-青岛埃仑通用科技有限公司 | 汽车水泵_汽车水泵厂家-瑞安市骏迪汽车配件有限公司 | PE拉伸缠绕膜,拉伸缠绕膜厂家,纳米缠绕膜-山东凯祥包装 | 美国PARKER齿轮泵,美国PARKER柱塞泵,美国PARKER叶片泵,美国PARKER电磁阀,美国PARKER比例阀-上海维特锐实业发展有限公司二部 | 泵阀展|阀门展|水泵展|流体机械展 -2025上海国际泵管阀展览会flowtech china | 护腰带生产厂家_磁石_医用_热压护腰_登山护膝_背姿矫正带_保健护具_医疗护具-衡水港盛 | 塑胶跑道_学校塑胶跑道_塑胶球场_运动场材料厂家_中国塑胶跑道十大生产厂家_混合型塑胶跑道_透气型塑胶跑道-广东绿晨体育设施有限公司 | 磨煤机配件-高铬辊套-高铬衬板-立磨辊套-盐山县宏润电力设备有限公司 | 济南展厅设计施工_数字化展厅策划设计施工公司_山东锐尚文化传播有限公司 | b2b网站大全,b2b网站排名,找b2b网站就上地球网 | 深圳希玛林顺潮眼科医院(官网)│深圳眼科医院│医保定点│香港希玛林顺潮眼科中心连锁品牌 | 广东机电安装工程_中央空调工程_东莞装饰装修-广东粤标建设有限公司 | 知企服务-企业综合服务(ZiKeys.com)-品优低价、种类齐全、过程管理透明、速度快捷高效、放心服务,知企专家! | 鑫达滑石-辽宁鑫达滑石集团 | 餐饮加盟网_特色餐饮连锁加盟店-餐饮加盟官网 | 照相馆预约系统,微信公众号摄影门店系统,影楼管理软件-盟百网络 | 合肥仿石砖_合肥pc砖厂家_合肥PC仿石砖_安徽旭坤建材有限公司 | 沈阳激光机-沈阳喷码机-沈阳光纤激光打标机-沈阳co2激光打标机 |