Это 120-я оригинальная статья без воды.Если вы хотите получить больше оригинальных статей, выполните поиск в общедоступном аккаунте и подпишитесь на нас~ Эта статья была впервые опубликована в блоге Zhengcaiyun:Почему не рекомендуется использовать индекс в качестве ключа в Vue
предисловие
Во фронтенд-разработке, пока задействован рендеринг списка, платформы React и Vue будут запрашивать или требовать, чтобы каждый элемент списка использовал уникальный ключ, и многие разработчики будут вместо этого напрямую использовать индекс массива в качестве значения ключа. Знать принцип ключа. Затем в этой статье объясняется роль ключа и почему лучше не использовать индекс в качестве значения атрибута ключа.
Роль ключа
Vue использует виртуальный DOM и сравнивает старый и новый DOM в соответствии с алгоритмом сравнения для обновления реального DOM.Ключ является уникальным идентификатором объекта виртуального DOM, и ключ играет чрезвычайно важную роль в алгоритме сравнения.
Роль ключа в алгоритме diff
На самом деле в React, Vue алгоритм сравнения примерно одинаков, но метод сравнения различий все же сильно отличается, и даже каждая версия diff сильно отличается. Давайте возьмем алгоритм сравнения Vue3.0 в качестве точки входа для анализа роли ключа в алгоритме сравнения.
Конкретный процесс сравнения выглядит следующим образом
В 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 выполняет реальное изменение или прямое исправление в зависимости от того, существует ли ключ. Для случая, когда ключа не существует, мы не будем проводить углубленное исследование.
Давайте сначала посмотрим на некоторые объявленные переменные.
/* c1 老的 vnode c2 新的vnode */
let i = 0 /* 记录索引 */
const l2 = c2.length /* 新 vnode的数量 */
let e1 = c1.length - 1 /* 老 vnode 最后一个节点的索引 */
let e2 = l2 - 1 /* 新节点最后一个节点的索引 */
головной узел синхронизации
Первым делом нужно с начала найти такой же vnode, а потом патчить, если обнаружится, что это не тот node, то сразу выпрыгнуть из цикла.
//(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
}
На самом деле, увидев это, вы уже знаете роль ключа в алгоритме сравнения, который используется для определения того, является ли это одним и тем же узлом.
Синхронизация хвостового узла
Второй шаг такой же, как предыдущий 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 может оставаться неизменным, можно обнаружить, что порядок d e в старом и новом узлах неизменен, 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, new, 0]. По сути, это идея перемещения диффа.
почему бы не использовать индекс
потребление производительности
При использовании индекса в качестве ключа, когда последовательная операция уничтожается, поскольку каждый узел не может найти соответствующий ключ, некоторые узлы нельзя использовать повторно, и все новые vnodes необходимо создавать заново.
пример:
<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, чтобы увидеть, как оно сравнивается.
Когда мы добавим фрагмент данных на передний план, порядок индекса будет прерван, что приведет к изменению всех новых ключей узлов, поэтому данные на нашей странице будут повторно отображаться.
Затем мы создадим 1000 DOM, чтобы сравнить производительность с использованием индекса и без него.Чтобы обеспечить уникальность ключа, мы используем uuid в качестве ключа.
Мы используем индекс в качестве ключа и выполняем его снова
<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>
Изменить на идентификатор в качестве ключа
<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
},
Как видно из приведенного выше сравнения, использование уникального значения в качестве ключа может сократить накладные расходы.
несовпадение данных
В приведенном выше примере вы можете подумать, что использование индекса в качестве ключа влияет только на эффективность загрузки страницы, а небольшое количество данных мало на что влияет.В следующей ситуации могут возникнуть непредвиденные проблемы при использовании индекса. описанный выше сценарий, то я сначала добавляю поле ввода после каждого текстового содержимого и вручную заполняю некоторое содержимое в поле ввода, а затем добавляю одноклассника вперед через кнопку, чтобы увидеть
<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>
Вводим какие-то значения на вход и добавляем одноклассник, чтобы увидеть эффект:
В это время мы обнаружим, что данные, введенные перед добавлением, неуместны. После добавления информация Чжан Саня остается в поле ввода Ван Ву, что, очевидно, не является тем результатом, который нам нужен.
Из вышеприведенного сравнения видно, что при использовании индекса в качестве ключа при сравнении обнаруживается, что хотя текстовое значение изменилось, оно обнаруживается при продолжении сравнения вниз.Узел DOM по-прежнему такой же, как исходный, и он используется повторно, но я не ожидал, что входное значение останется в поле ввода, и в это время входное значение будет неуместно.
решение
Теперь, когда мы знаем, что использование индекса в некоторых случаях может иметь очень плохие последствия, как мы обычно решаем эту ситуацию во время разработки? На самом деле, пока вы гарантируете, что ключ уникален и неизменен, он обычно используется в следующих трех ситуациях в процессе разработки.
- В разработке лучше всего использовать данные с уникальной идентификацией и фиксированные данные в качестве ключа для каждого фрагмента данных, например, идентификатор, номер мобильного телефона, идентификационный номер и другие уникальные значения, возвращаемые фоном.
- Символ можно использовать в качестве ключа.Символ — это новый примитивный тип данных Symbol, введенный в ES6, который представляет собой уникальное значение.Самое широкое использование — определение уникального имени свойства объекта.
let a=Symbol('测试')
let b=Symbol('测试')
console.log(a===b)//false
- Вы можете использовать uuid в качестве ключа, uuid — это аббревиатура универсального уникального идентификатора, который представляет собой сгенерированный машиной идентификатор, уникальный в определенном диапазоне (от определенного пространства имен до всего мира).
Мы используем первую схему выше в качестве ключа, чтобы взглянуть на вышеприведенную ситуацию, как показано на рисунке. Узлы с одним и тем же ключом используются повторно. Настоящую роль сыграл алгоритм diff.
Суммировать
- Когда индекс используется в качестве ключа, ненужные реальные обновления DOM будут генерироваться при добавлении данных в обратном порядке, удалении в обратном порядке и других операциях, которые нарушают порядок, что приводит к низкой эффективности.
- Использование индекса в качестве ключа приведет к некорректному обновлению DOM, если структура содержит DOM входного класса.
- В разработке лучше всего использовать данные с уникальной идентификацией и фиксированные данные в качестве ключа для каждого фрагмента данных, например, идентификатор, номер мобильного телефона, идентификационный номер и другие уникальные значения, возвращаемые фоном.
- Если нет операций, нарушающих порядок данных, таких как добавление и удаление данных в обратном порядке, также возможно использовать индекс в качестве ключа, когда он используется только для рендеринга и отображения (но не рекомендуется использовать его , и развивать хорошие привычки развития).
Ссылка на ссылку
Подробное объяснение алгоритма сравнения vue3.0 (очень подробное)
Чтение исходного кода Vue 3 Virtual Dom Diff
Рекомендуемое чтение
Минимальные запасы для электронной коммерции — артикул и реализация алгоритма
Что нужно знать об управлении проектами
Перекомпоновка и перерисовка рендеринга браузера
Сценарии и приложения для предотвращения дрожания
работы с открытым исходным кодом
- Zhengcaiyun интерфейсный таблоид
адрес с открытым исходным кодомwww.zoo.team/openweekly/(На главной странице официального сайта таблоида есть группа обмена WeChat)
- skuDemo
адрес с открытым исходным кодомGitHub.com/Chinese Patent Medicine-Inc/Reservoir…
Карьера
ZooTeam, молодая, увлеченная и творческая команда, связанная с отделом исследований и разработок продукции Zhengcaiyun, базируется в живописном Ханчжоу. В настоящее время в команде более 50 фронтенд-партнеров, средний возраст которых составляет 27 лет, и почти 30% из них — инженеры с полным стеком, настоящая молодежная штурмовая группа. В состав членов входят «ветераны» солдат из Ali и NetEase, а также первокурсники из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других школ. В дополнение к ежедневным деловым связям, команда также проводит технические исследования и фактические боевые действия в области системы материалов, инженерной платформы, строительной платформы, производительности, облачных приложений, анализа и визуализации данных, а также продвигает и внедряет ряд внутренних технологий. Откройте для себя новые горизонты передовых технологических систем.
Если вы хотите измениться, вас забрасывают вещами, и вы надеетесь начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите изменить , у вас есть возможность добиться этого результата, но вы не нужны; если вы хотите изменить то, чего хотите достичь, вам нужна команда для поддержки, но вам некуда вести людей; если вы хотите изменить установившийся ритм, это будет "5 лет рабочего времени и 3 года стажа работы"; если вы хотите изменить исходный Понимание хорошее, но всегда есть размытие того слоя оконной бумаги.. , Если вы верите в силу веры, верьте, что обычные люди могут достичь необыкновенных вещей, и верьте, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе становления бизнеса и лично способствовать росту фронтенд-команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, что мы должны говорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наZooTeam@cai-inc.com