Ключи списка и независимость карточек
Оглавление серии «Vue 3: кабинет сотрудников «Команда»»
Фильтр меняет состав видимых строк; сортировка изменит их порядок. Добавим выбор направления сортировки по имени и небольшое поле заметки в каждой строке. Поле здесь намеренно остаётся обычным элементом браузера без v-model. Оно поможет объяснить, что идентичность строки включает не только текст сотрудника, но и состояние находящегося в ней элемента. Результат урока — правильный ключ списка и сортировка без изменения исходной коллекции.
Представьте, что вы написали «Обсудить макет» возле Анны, а затем поменяли порядок имён. Эта заметка должна остаться возле Анны. Если ключом станет позиция в массиве, Vue может повторно использовать первый элемент для другого человека. Сама сортировка при этом будет правильной, но пользователь увидит чужую заметку. Именно такой ошибочный случай делает договор key предметным, а не декоративным правилом оформления.
Копия перед сортировкой
Добавим новый ref descending. Вычисляемое значение сначала получает отфильтрованный массив, затем сортирует его. Метод sort изменяет тот массив, на котором вызывается. Поэтому важно, что filter уже создал отдельную коллекцию: исходный порядок members остаётся нетронутым. Если фильтр когда-нибудь уберут, перед sort понадобится явная копия через spread или другой подходящий метод. Нельзя считать sort автоматически безопасным для общего источника.
Ниже полностью заменяется src/App.vue. Это учебный вариант для исследования идентичности элементов; долговременные заметки ещё не являются частью модели сотрудника.
<script setup>
import { computed, ref } from 'vue'
import { seedMembers } from './data/members.js'
const members = ref(seedMembers.map(member => ({ ...member })))
const query = ref('')
const onlyAvailable = ref(false)
const descending = ref(false)
const visibleMembers = computed(() => {
const term = query.value.trim().toLocaleLowerCase('ru')
return members.value.filter(member =>
`${member.name} ${member.role}`.toLocaleLowerCase('ru').includes(term)
&& (!onlyAvailable.value || member.available)
).sort((a, b) => {
const order = a.name.localeCompare(b.name, 'ru') || a.id.localeCompare(b.id)
return descending.value ? -order : order
})
})
function toggle(id) {
const member = members.value.find(item => item.id === id)
if (member) member.available = !member.available
}
</script>
<template>
<main>
<h1>Команда</h1>
<label>Поиск <input v-model="query" type="search"></label>
<label><input v-model="onlyAvailable" type="checkbox"> Только доступные</label>
<label><input v-model="descending" type="checkbox"> Обратный порядок</label>
<p>Показано {{ visibleMembers.length }} из {{ members.length }}</p>
<p v-if="!visibleMembers.length">Подходящих участников нет.</p>
<ul>
<li v-for="member in visibleMembers" :key="member.id">
{{ member.name }} — {{ member.role }}
<button type="button" @click="toggle(member.id)">
{{ member.available ? 'Доступен' : 'Занят' }}: {{ member.name }}
</button>
<label>Временная заметка для {{ member.name }} <input type="text"></label>
</li>
</ul>
</main>
</template>
При обычном порядке ожидаются Анна, Ирина, Павел. При обратном — Павел, Ирина, Анна. Для одинаковых имён второе сравнение по id задаёт дополнительный порядок. Это не замена ключу и не выдача идентификатора пользователю: сравнение отвечает за расположение, key отвечает за сопоставление элементов между обновлениями. Две эти операции решают разные задачи и не могут надёжно подменить друг друга.
Идентификатор как договор строки
Ключ должен быть уникальным среди соседних элементов и сохраняться у той же записи. name не подходит, потому что имена могут повторяться и изменяться. available не подходит ещё очевиднее: значений всего два. Случайное число при каждом вычислении также неверно: Vue будет получать новую личность строки при каждом обновлении. Наш id заранее существует в данных и переживает поиск, сортировку и переключение доступности.
Руководство по спискам Vue описывает сопоставление состояния элементов через key. Применительно к кабинету это означает, что строка m-1 должна оставаться строкой Анны при изменении её позиции. Не выводите из этого правило о сохранении всего навсегда: если фильтр полностью убирает строку из DOM, находящийся внутри обычный input может быть уничтожен. При возвращении строки Vue создаст элемент заново.
Поэтому поле названо временной заметкой. Поворот списка должен сохранить его рядом с тем же участником, но скрытие строки и последующее возвращение не обязаны сохранить текст. Если заметка важна пользователю, её следует разместить в отдельном реактивном черновике, связанном с id. Ключ предотвращает неправильное сопоставление DOM, однако не превращает несохранённое браузерное значение в устойчивую запись. Это существенное ограничение часто скрывается за коротким советом «всегда пишите key».
Для изучения ошибки можно в собственной копии временно заменить :key="member.id" на :key="index" и добавить index в выражение v-for. Заполните первое поле и разверните порядок. Возможное неверное сопоставление заметки объясняется повторным использованием позиции. После разбора верните id. В основной версии архива хранится правильный вариант; ошибочная модификация приведена как исследование причины, а не как дополнительный рабочий снимок.
Стоимость и поведение сортировки
Сортировка выполняется над текущими совпадениями. При изменении query Vue пересчитывает список; при изменении descending используется то же правило сравнения с другим знаком. На маленькой команде не требуется оптимизация или отдельный индекс. Для большого набора следует сначала уточнить договор загрузки и количество строк. Сам факт наличия computed не означает, что фильтрация и сортировка бесплатны: кеш помогает при неизменных зависимостях, но новое условие требует нового вычисления.
localeCompare с русской локалью задаёт сравнение строк средствами среды. Этот урок не обещает одинаковой тонкой колляции на любом сервере и клиенте. Если порядок должен совпадать с пагинацией API, его следует определить серверным договором и не пересортировывать отдельные страницы произвольно. Пока весь набор локальный и небольшой, такое сравнение достаточно для понятного учебного результата. Закреплённый id остаётся полезным при любом будущем способе упорядочивания.
Форма фильтров пока не отправляет запрос: каждое изменение сразу меняет представление данных. По этой причине чекбоксы не требуют общей кнопки «Применить». Если пользовательские условия начнут запускать дорогую серверную операцию, интерфейс может перейти к подтверждению или задержке ввода. Не добавляйте такое поведение только потому, что оно встречается в других кабинетах. Здесь действие остаётся синхронным преобразованием уже доступной коллекции.
Проверьте понимание без запуска: запишите id трёх строк до разворота и после него. Значения id должны остаться теми же, только последовательность изменится. Затем объясните различие между разворотом и фильтром, который исключает Анну. В первом случае элемент переезжает, во втором перестаёт существовать в текущем представлении. Следующий урок перенесёт содержимое строки в компонент MemberCard и сохранит тот же договор личности, добавив явную границу свойств и событий.
Равные имена и выбор записи
Допустим, в команду добавлена ещё одна Анна. Сортировка покажет две одинаковые подписи, но их id различаются. Кнопка каждой строки передаёт собственный id, поэтому изменение не должно затрагивать первую найденную запись по имени. Это хороший способ проверить согласованность модели: поиск по подписи может вернуть несколько людей, действие всегда относится к одной записи. Если пользователь не способен различить одинаковые имена, интерфейсу потребуется дополнительное видимое поле, например отдел. Идентификатор решает техническую однозначность, но сам по себе не помогает человеку выбрать нужного сотрудника. Не используйте случайный порядок как замену ясному представлению.
Для самостоятельного воспроизведения результата «Сохранить идентичность строк при изменении порядка» выберите lesson-04 в архиве исходников. Статус выполнения этого состояния: not_run.