Директива v-for позволяет рендерить список элементов на основе массива. Синтаксис: v-for="item in items", где items — исходный массив, а item — псевдоним для каждого элемента.
<template>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
</template>
<script setup>
const tasks = ref([
{ id: 1, title: 'Изучить Vue 3' },
{ id: 2, title: 'Написать компонент' },
{ id: 3, title: 'Задеплоить приложение' },
])
</script>Вторым параметром можно получить текущий индекс:
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>v-for работает и с объектами. Три параметра: значение, ключ, индекс:
<template>
<dl>
<template v-for="(value, key, index) in user" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</dl>
</template>
<script setup>
const user = reactive({
name: 'Анна',
age: 28,
city: 'Москва',
})
</script>v-for может принимать число — итерирует от 1 до N:
<!-- Выведет: 1 2 3 4 5 -->
<span v-for="n in 5" :key="n">{{ n }} </span>Атрибут :key обязателен при v-for. Vue использует его для **отслеживания идентичности элементов** при обновлении списка.
Без key Vue применяет стратегию «патчинг на месте»: при изменении порядка он просто обновляет контент каждого существующего DOM-узла, не перемещая их. Это дешевле, но может привести к ошибкам со state компонентов, фокусом полей ввода, анимациями.
С уникальным key Vue точно знает, какой элемент соответствует какому DOM-узлу, и может корректно добавлять, удалять и перемещать их.
<!-- Плохо: индекс как key — при удалении/сортировке всё ломается -->
<li v-for="(item, index) in items" :key="index">
<!-- Хорошо: уникальный стабильный ID -->
<li v-for="item in items" :key="item.id">В Vue 3 v-if имеет **более высокий приоритет**, чем v-for. Поэтому нельзя использовать их на одном элементе (переменные из v-for будут недоступны в v-if).
Правильный подход — обернуть в <template>:
<!-- Неправильно в Vue 3: v-if не имеет доступа к item -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
<!-- Правильно: v-for снаружи, v-if внутри -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>Или ещё лучше — фильтруй данные через computed-свойство и не смешивай директивы.
Демонстрация работы :key при обновлении списка: с ключом и без ключа
// Эмулируем алгоритм Vue при обновлении v-for списка.
// Показываем разницу между "patch in place" (без key) и "keyed diff" (с key).
// --- Без :key: патчинг на месте ---
console.log('=== Обновление списка БЕЗ :key ===')
function patchInPlace(oldList, newList) {
const ops = []
const len = Math.max(oldList.length, newList.length)
for (let i = 0; i < len; i++) {
if (i >= newList.length) {
ops.push(`Удалить DOM-узел на позиции ${i}`)
} else if (i >= oldList.length) {
ops.push(`Создать новый DOM-узел: "${newList[i].name}"`)
} else if (oldList[i].id !== newList[i].id) {
ops.push(`Патч узла [${i}]: заменить "${oldList[i].name}" -> "${newList[i].name}" (перезаписать контент)`)
} else {
ops.push(`Узел [${i}] не изменился: "${oldList[i].name}"`)
}
}
return ops
}
const original = [
{ id: 1, name: 'Яблоко' },
{ id: 2, name: 'Банан' },
{ id: 3, name: 'Вишня' },
]
// Удаляем первый элемент — остальные сдвигаются
const afterDelete = [
{ id: 2, name: 'Банан' },
{ id: 3, name: 'Вишня' },
]
patchInPlace(original, afterDelete).forEach(op => console.log(' ', op))
console.log('Результат: Vue перезаписал содержимое первых двух узлов вместо удаления одного')
// --- С :key: умный diff ---
console.log('\n=== Обновление списка С :key ===')
function keyedDiff(oldList, newList) {
const oldMap = new Map(oldList.map(item => [item.id, item]))
const newMap = new Map(newList.map(item => [item.id, item]))
const ops = []
// Найти удалённые
for (const [id, item] of oldMap) {
if (!newMap.has(id)) {
ops.push(`Удалить DOM-узел для id=${id} ("${item.name}")`)
}
}
// Найти добавленные и перемещённые
newList.forEach((item, newIndex) => {
if (!oldMap.has(item.id)) {
ops.push(`Создать новый DOM-узел для id=${item.id} ("${item.name}") на позиции ${newIndex}`)
} else {
const oldIndex = oldList.findIndex(i => i.id === item.id)
if (oldIndex !== newIndex) {
ops.push(`Переместить id=${item.id} ("${item.name}") с позиции ${oldIndex} на ${newIndex}`)
} else {
ops.push(`id=${item.id} ("${item.name}") остался на месте`)
}
}
})
return ops
}
keyedDiff(original, afterDelete).forEach(op => console.log(' ', op))
console.log('Результат: Vue точно удалил один узел, остальные не тронул')
// --- Почему индекс как key опасен ---
console.log('\n=== Почему :key="index" — плохая практика ===')
const withInput = [
{ id: 1, name: 'Задача A', userInput: 'мой черновик' },
{ id: 2, name: 'Задача B', userInput: '' },
{ id: 3, name: 'Задача C', userInput: '' },
]
const afterDeleteFirst = withInput.slice(1)
console.log('До удаления: input[0].userInput =', withInput[0].userInput)
console.log('После удаления элемента id=1:')
console.log(' С key=index: DOM-узел с userInput="мой черновик" остался на позиции 0')
console.log(' Теперь он отображает "Задача B" — но черновик не пропал!')
console.log(' С key=id: Vue удалил правильный узел, черновик исчез вместе с задачей A')Директива v-for позволяет рендерить список элементов на основе массива. Синтаксис: v-for="item in items", где items — исходный массив, а item — псевдоним для каждого элемента.
<template>
<ul>
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
</template>
<script setup>
const tasks = ref([
{ id: 1, title: 'Изучить Vue 3' },
{ id: 2, title: 'Написать компонент' },
{ id: 3, title: 'Задеплоить приложение' },
])
</script>Вторым параметром можно получить текущий индекс:
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>v-for работает и с объектами. Три параметра: значение, ключ, индекс:
<template>
<dl>
<template v-for="(value, key, index) in user" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</dl>
</template>
<script setup>
const user = reactive({
name: 'Анна',
age: 28,
city: 'Москва',
})
</script>v-for может принимать число — итерирует от 1 до N:
<!-- Выведет: 1 2 3 4 5 -->
<span v-for="n in 5" :key="n">{{ n }} </span>Атрибут :key обязателен при v-for. Vue использует его для **отслеживания идентичности элементов** при обновлении списка.
Без key Vue применяет стратегию «патчинг на месте»: при изменении порядка он просто обновляет контент каждого существующего DOM-узла, не перемещая их. Это дешевле, но может привести к ошибкам со state компонентов, фокусом полей ввода, анимациями.
С уникальным key Vue точно знает, какой элемент соответствует какому DOM-узлу, и может корректно добавлять, удалять и перемещать их.
<!-- Плохо: индекс как key — при удалении/сортировке всё ломается -->
<li v-for="(item, index) in items" :key="index">
<!-- Хорошо: уникальный стабильный ID -->
<li v-for="item in items" :key="item.id">В Vue 3 v-if имеет **более высокий приоритет**, чем v-for. Поэтому нельзя использовать их на одном элементе (переменные из v-for будут недоступны в v-if).
Правильный подход — обернуть в <template>:
<!-- Неправильно в Vue 3: v-if не имеет доступа к item -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
<!-- Правильно: v-for снаружи, v-if внутри -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>Или ещё лучше — фильтруй данные через computed-свойство и не смешивай директивы.
Демонстрация работы :key при обновлении списка: с ключом и без ключа
// Эмулируем алгоритм Vue при обновлении v-for списка.
// Показываем разницу между "patch in place" (без key) и "keyed diff" (с key).
// --- Без :key: патчинг на месте ---
console.log('=== Обновление списка БЕЗ :key ===')
function patchInPlace(oldList, newList) {
const ops = []
const len = Math.max(oldList.length, newList.length)
for (let i = 0; i < len; i++) {
if (i >= newList.length) {
ops.push(`Удалить DOM-узел на позиции ${i}`)
} else if (i >= oldList.length) {
ops.push(`Создать новый DOM-узел: "${newList[i].name}"`)
} else if (oldList[i].id !== newList[i].id) {
ops.push(`Патч узла [${i}]: заменить "${oldList[i].name}" -> "${newList[i].name}" (перезаписать контент)`)
} else {
ops.push(`Узел [${i}] не изменился: "${oldList[i].name}"`)
}
}
return ops
}
const original = [
{ id: 1, name: 'Яблоко' },
{ id: 2, name: 'Банан' },
{ id: 3, name: 'Вишня' },
]
// Удаляем первый элемент — остальные сдвигаются
const afterDelete = [
{ id: 2, name: 'Банан' },
{ id: 3, name: 'Вишня' },
]
patchInPlace(original, afterDelete).forEach(op => console.log(' ', op))
console.log('Результат: Vue перезаписал содержимое первых двух узлов вместо удаления одного')
// --- С :key: умный diff ---
console.log('\n=== Обновление списка С :key ===')
function keyedDiff(oldList, newList) {
const oldMap = new Map(oldList.map(item => [item.id, item]))
const newMap = new Map(newList.map(item => [item.id, item]))
const ops = []
// Найти удалённые
for (const [id, item] of oldMap) {
if (!newMap.has(id)) {
ops.push(`Удалить DOM-узел для id=${id} ("${item.name}")`)
}
}
// Найти добавленные и перемещённые
newList.forEach((item, newIndex) => {
if (!oldMap.has(item.id)) {
ops.push(`Создать новый DOM-узел для id=${item.id} ("${item.name}") на позиции ${newIndex}`)
} else {
const oldIndex = oldList.findIndex(i => i.id === item.id)
if (oldIndex !== newIndex) {
ops.push(`Переместить id=${item.id} ("${item.name}") с позиции ${oldIndex} на ${newIndex}`)
} else {
ops.push(`id=${item.id} ("${item.name}") остался на месте`)
}
}
})
return ops
}
keyedDiff(original, afterDelete).forEach(op => console.log(' ', op))
console.log('Результат: Vue точно удалил один узел, остальные не тронул')
// --- Почему индекс как key опасен ---
console.log('\n=== Почему :key="index" — плохая практика ===')
const withInput = [
{ id: 1, name: 'Задача A', userInput: 'мой черновик' },
{ id: 2, name: 'Задача B', userInput: '' },
{ id: 3, name: 'Задача C', userInput: '' },
]
const afterDeleteFirst = withInput.slice(1)
console.log('До удаления: input[0].userInput =', withInput[0].userInput)
console.log('После удаления элемента id=1:')
console.log(' С key=index: DOM-узел с userInput="мой черновик" остался на позиции 0')
console.log(' Теперь он отображает "Задача B" — но черновик не пропал!')
console.log(' С key=id: Vue удалил правильный узел, черновик исчез вместе с задачей A')Напиши функцию `filterAndIndex(items, predicate)`, которая принимает массив объектов и функцию-предикат, и возвращает новый массив только с теми элементами, для которых предикат возвращает `true`. Каждый элемент результата должен содержать исходный объект и его оригинальный индекс в поле `originalIndex`. Это эмулирует использование computed-свойства вместо v-if внутри v-for.
Используй `items.reduce((acc, item, index) => { if (predicate(item)) acc.push({ item, originalIndex: index }); return acc }, [])`, или сначала map с индексом, затем filter.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке