Vue에서 인덱스를 키로 사용하지 않는 것이 왜 권장되지 않나요? 다음 기사에서는 그 이유를 분석하여 여러분에게 도움이 되기를 바랍니다.
프런트엔드 개발에서는 목록 렌더링이 포함되는 한 React든 Vue 프레임워크든 각 목록 항목에 고유 키를 사용하라는 메시지가 표시되거나 요구되므로 많은 개발자가 직접 인덱스를 사용하게 됩니다. 키의 원리를 모르고 배열을 키로 사용합니다. 그런 다음 이 기사에서는 키의 역할과 인덱스를 키의 속성 값으로 사용하지 않는 것이 가장 좋은 이유를 설명합니다.
Vue는 가상 DOM을 사용하고 diff 알고리즘에 따라 이전 DOM과 새 DOM을 비교하여 실제 DOM을 업데이트합니다. 키는 가상 DOM 개체의 고유 식별자입니다. 매우 중요한 역할. [관련 추천: "vue.js Tutorial"] diff 알고리즘에서
사실 React와 Vue의 diff 알고리즘은 대략 동일하지만 diff 비교 방법은 여전히 상당히 다릅니다. 심지어 diff의 각 버전은 상당히 다릅니다. 다음으로 Vue3.0 diff 알고리즘을 출발점으로 사용하여 diff 알고리즘에서 키의 역할을 분석하겠습니다. Vue3.0의 경우 patchChildren 메소드에 이러한 소스 코드가 있습니다.
if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { /* 对于存在 key 的情况用于 diff 算法 */ patchKeyedChildren( ... ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { /* 对于不存在 key 的情况,直接 patch */ patchUnkeyedChildren( ... ) return } }
patchChildren 실제 diff를 수행할지 직접 패치를 수행할지에 대한 키가 있는지에 따라. 키가 존재하지 않는 경우는 다루지 않겠습니다.
먼저 선언된 변수를 살펴보겠습니다.
/* c1 老的 vnode c2 新的vnode */ let i = 0 /* 记录索引 */ const l2 = c2.length /* 新 vnode的数量 */ let e1 = c1.length - 1 /* 老 vnode 最后一个节点的索引 */ let e2 = l2 - 1 /* 新节点最后一个节点的索引 */
헤드 노드 동기화
첫 번째 단계는 처음부터 동일한 vnode를 찾아 패치하는 것입니다. 동일한 노드가 아닌 것으로 확인되면 즉시 루프에서 빠져나옵니다.//(a b) c //(a b) d e /* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/ while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) /* 判断 key ,type 是否相等 */ if (isSameVNodeType(n1, n2)) { patch( ... ) } else { break } i++ }
isSameVNodeType은 현재 vnode 유형과 vnode의 키가 동일한지 확인하는 데 사용됩니다.
export function isSameVNodeType(n1: VNode, n2: VNode): boolean { return n1.type === n2.type && n1.key === n2.key }
사실 이것을 보고 나면 이미 key의 역할을 알고 계시나요? diff 알고리즘은 동일한 노드인지 여부를 결정하는 데 사용됩니다.
테일 노드 동기화
두 번째 단계는 diff 끝부터 동일합니다//a (b c) //d e (b c) /* 如果第一步没有 patch 完,立即,从后往前开始 patch 如果发现不同立即跳出循环 */ while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( ... ) } else { break } e1-- e2-- }
3단계: 이전 노드가 모두 패치되고 새 노드가 패치되지 않은 경우 새 vnode를 만듭니다
//(a b) //(a b) c //i = 2, e1 = 1, e2 = 2 //(a b) //c (a b) //i = 0, e1 = -1, e2 = 0 /* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的 vnode 处理(这种情况说明已经 patch 完相同的 vnode ) */ if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( /* 创建新的节点*/ ... ) i++ } } }
4단계: 새 노드가 모두 패치되고 기존 노드가 남아 있는 경우 기존 노드를 모두 제거합니다.
//i > e2 //(a b) c //(a b) //i = 2, e1 = 2, e2 = 1 //a (b c) //(b c) //i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } }
이 단계에서는 비교 핵심 장면이 아직 나타나지 않았습니다. 운이 좋으면 여기서 끝날 수도 있지만 운에만 의존할 수는 없습니다. 나머지 시나리오는 이전 노드와 새 노드 모두에 여러 하위 노드가 있는 경우입니다. 그러면 Vue3가 어떻게 작동하는지 살펴보겠습니다. 이동 작업을 결합하려면 추가 및 제거
위의 예를 보면 c h d e VS d e i c입니다. 비교해 보면 c를 맨 끝으로 이동한 다음 h를 제거하고 i를 추가하면 된다는 것을 육안으로 확인할 수 있습니다. d e는 변경되지 않은 상태로 유지될 수 있으며, 이전 노드와 새 노드에서 de의 순서는 변경되지 않고 d가 e 뒤에 있고 첨자가 증가하는 상태에 있음을 알 수 있습니다.
这里引入一个概念,叫最长递增子序列。 官方解释:在一个给定的数组中,找到一组递增的数值,并且长度尽可能的大。 有点比较难理解,那来看具体例子: const arr = [10, 9, 2, 5, 3, 7, 101, 18] => [2, 3, 7, 18] 这一列数组就是arr的最长递增子序列,其实[2, 3, 7, 101]也是。 所以最长递增子序列符合三个要求: 1、子序列内的数值是递增的 2、子序列内数值的下标在原数组中是递增的 3、这个子序列是能够找到的最长的 但是我们一般会找到数值较小的那一组数列,因为他们可以增长的空间会更多。
那接下来的思路是:如果能找到老节点在新节点序列中顺序不变的节点们,就知道,哪一些节点不需要移动,然后只需要把不在这里的节点插入进来就可以了。**因为最后要呈现出来的顺序是新节点的顺序,移动是只要老节点移动,所以只要老节点保持最长顺序不变,通过移动个别节点,就能够跟它保持一致。**所以在此之前,先把所有节点都找到,再找对应的序列。最后其实要得到的则是这一个数组:[2, 3, 新增 , 0]。其实这就是 diff 移动的思路了
使用 index 做 key,破坏顺序操作的时候, 因为每一个节点都找不到对应的 key,导致部分节点不能复用,所有的新 vnode 都需要重新创建。
例子:
<template> <div class="hello"> <ul> <li v-for="(item,index) in studentList" :key="index">{{item.name}}</li> <br> <button @click="addStudent">添加一条数据</button> </ul> </div> </template> <script> export default { name: 'HelloWorld', data() { return { studentList: [ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 19 }, ], }; }, methods:{ addStudent(){ const studentObj = { id: 3, name: '王五', age: 20 }; this.studentList=[studentObj,...this.studentList] } } } </script>
我们先把 Chorme 调试器打开,我们双击把里面文本修改一下
我们运行以上上面的代码,看下运行结果
从上面运行结果可以看出来,我们只是添加了一条数据,但是三条数据都需要重新渲染是不是很惊奇,我明明只是插入了一条数据,怎么三条数据都要重新渲染?而我想要的只是新增的那一条数据新渲染出来就行了。
上面我们也讲过 diif 比较方式,下面根据 diff 比较绘制一张图,看看具体是怎么比较的吧
当我们在前面加了一条数据时 index 顺序就会被打断,导致新节点 key 全部都改变了,所以导致我们页面上的数据都被重新渲染了。
下面我们下面生成1000个 DOM 来比较一下采用 index ,和不采用 index 性能比较,为了保证 key 的唯一性我们采用 uuid 作为 key
我们用 index 做为 key 现执行一遍
<template> <div class="hello"> <ul> <button @click="addStudent">添加一条数据</button> <br> <li v-for="(item,index) in studentList" :key="index">{{item.id}}</li> </ul> </div> </template> <script> import uuidv1 from 'uuid/v1' export default { name: 'HelloWorld', data() { return { studentList: [{id:uuidv1()}], }; }, created(){ for (let i = 0; i < 1000; i++) { this.studentList.push({ id: uuidv1(), }); } }, beforeUpdate(){ console.time('for'); }, updated(){ console.timeEnd('for')//for: 75.259033203125 ms }, methods:{ addStudent(){ const studentObj = { id: uuidv1() }; this.studentList=[studentObj,...this.studentList] } } } </script>
换成 id 作为 key
<template> <div class="hello"> <ul> <button @click="addStudent">添加一条数据</button> <br> <li v-for="(item,index) in studentList" :key="item.id">{{item.id}}</li> </ul> </div> </template> beforeUpdate(){ console.time('for'); }, updated(){ console.timeEnd('for')//for: 42.200927734375 ms },
从上面比较可以看出,用唯一值作为 key 可以节约开销
上述例子可能觉得用 index 做 key 只是影响页面加载的效率,认为少量的数据影响不大,那面下面这种情况,可能用 index 就可能出现一些意想不到的问题了,还是上面的场景,这时我先再每个文本内容后面加一个 input 输入框,并且手动在输入框内填写一些内容,然后通过 button 向前追加一位同学看看
<template> <div class="hello"> <ul> <li v-for="(item,index) in studentList" :key="index">{{item.name}}<input /></li> <br> <button @click="addStudent">添加一条数据</button> </ul> </div> </template> <script> export default { name: 'HelloWorld', data() { return { studentList: [ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 19 }, ], }; }, methods:{ addStudent(){ const studentObj = { id: 3, name: '王五', age: 20 }; this.studentList=[studentObj,...this.studentList] } } } </script>
我们往 input 里面输入一些值,添加一位同学看下效果:
这时候我们就会发现,在添加之前输入的数据错位了。添加之后王五的输入框残留着张三的信息,这很显然不是我们想要的结果。
从上面比对可以看出来这时因为采用 index 作为 key 时,当在比较时,发现虽然文本值变了,但是当继续向下比较时发现DOM 节点还是和原来一摸一样,就复用了,但是没想到 input 输入框残留输入的值,这时候就会出现输入的值出现错位的情况
既然知道用 index 在某些情况下带来很不好的影响,那平时我们在开发当中怎么去解决这种情况呢?其实只要保证 key 唯一不变就行,一般在开发中用的比较多就是下面三种情况。
在开发中最好每条数据使用唯一标识固定的数据作为 key,比如后台返回的 ID,手机号,身份证号等唯一值
可以采用 Symbol 作为 key,Symbol 是 ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名。
let a=Symbol('测试') let b=Symbol('测试') console.log(a===b)//false
可以采用 uuid 作为 key ,uuid 是 Universally Unique Identifier 的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符
我们采用上面第一种方案作为 key 在看一下上面情况,如图所示。key 相同的节点都做到了复用。起到了diff 算法的真正作用。
저자: Zhengcaiyun 프론트 엔드 팀더 많은 프로그래밍 관련 지식을 보려면 다음을 방문하세요. 소개 프로그래밍
! !
위 내용은 Vue에서 인덱스를 핵심으로 권장하지 않는 이유에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!