Олег Марков
Директива v-for в Vue.js
Введение
Директива v-for в Vue.js отвечает за вывод списков в шаблоне. С ее помощью вы можете отрисовывать массивы, объекты, диапазоны чисел и даже результаты вычислений, повторяя фрагмент шаблона столько раз, сколько элементов в коллекции.
Смотрите, я покажу вам, как это работает: вы описываете шаблон одного элемента, а v-for дублирует его для каждого элемента массива (или ключа объекта), подставляя актуальные данные. Это основа для создания таблиц, списков, карточек товаров, меню и множества других интерфейсных блоков.
В этой статье мы шаг за шагом разберем:
- синтаксис v-for и разные варианты его записи
- перебор массивов, объектов, строк и диапазонов чисел
- работу с индексами, ключами и вложенными циклами
- оптимизацию производительности с помощью атрибута key
- типичные ошибки, которые часто допускают разработчики
Теперь давайте перейдем к деталям.
Базовый синтаксис v-for
Перебор массивов
Самый частый сценарий использования v-for — вывод массива объектов. Давайте разберемся на примере.
<ul>
<!-- Здесь мы перебираем массив users и выводим имя каждого пользователя -->
<li v-for="user in users">
{{ user.name }}
</li>
</ul>
export default {
data() {
return {
// Массив объектов, которые мы будем выводить в шаблоне
users: [
{ id: 1, name: 'Иван' },
{ id: 2, name: 'Мария' },
{ id: 3, name: 'Алексей' }
]
}
}
}
Как видите, синтаксис v-for очень похож на «for each» в других языках: user — это текущий элемент массива, а users — сама коллекция.
Vue автоматически:
- повторяет тег
liдля каждого элемента массива - подставляет в
userтекущий объект - обновляет список при изменении массива
Формат item in items и item of items
В Vue вы можете использовать два варианта:
item in itemsitem of items
Они эквивалентны. В документации чаще используется in, и в статье мы тоже будем придерживаться его, чтобы вам было проще ориентироваться.
Доступ к индексу элемента
Иногда вам нужен не только элемент массива, но и его индекс (позиция в массиве). Например, чтобы вывести порядковый номер строки в таблице.
Смотрите, я покажу вам, как добавить индекс:
<ul>
<!-- Здесь мы получаем и элемент user, и его индекс index -->
<li v-for="(user, index) in users">
{{ index + 1 }}. {{ user.name }}
</li>
</ul>
Комментарии:
user— текущий элемент массиваindex— его индекс, начиная с 0index + 1— чтобы списки в интерфейсе начинались с 1, а не с 0
Использование деструктуризации (Vue 3 + сборка)
В проектах на Vue 3 со сборкой (Vite, Webpack) вы можете использовать деструктуризацию в v-for. Это удобно, если вы часто обращаетесь к нескольким полям объекта:
<ul>
<!-- Здесь мы сразу извлекаем name и email из каждого объекта user -->
<li v-for="({ name, email }, index) in users" :key="email">
{{ index + 1 }} — {{ name }} ({{ email }})
</li>
</ul>
export default {
data() {
return {
users: [
{ name: 'Иван', email: 'ivan@example.com' },
{ name: 'Мария', email: 'maria@example.com' }
]
}
}
}
Так запись получается более компактной и читабельной, но важно понимать, что под капотом это тот же перебор массива.
Атрибут key и почему он критичен
Зачем нужен key
Когда вы работаете со списками, Vue старается минимизировать количество изменений в DOM. Чтобы делать это корректно, ему нужно понимать, какой элемент списка является каким. Для этого используется атрибут key.
Давайте посмотрим пример без key:
<ul>
<!-- Здесь у элементов списка нет ключа -->
<li v-for="user in users">
<input v-model="user.name" />
</li>
</ul>
Проблема: если вы добавите или удалите пользователя где-то в середине списка, Vue может «переиспользовать» DOM-элементы неправильно. В итоге значения в полях ввода могут «съехать» и оказаться у других пользователей.
Решение — добавить уникальный key:
<ul>
<!-- Здесь мы задаем уникальный ключ по полю user.id -->
<li v-for="user in users" :key="user.id">
<input v-model="user.name" />
</li>
</ul>
Комментарии:
:key— двоеточие означает привязку (binding) к выражениюuser.id— уникальное значение для каждого пользователя
Так Vue понимает, какой конкретно DOM-элемент связан с каким объектом, и обновляет только нужные узлы.
Какие ключи использовать
Лучший вариант — устойчивый уникальный идентификатор:
- поле
idиз базы данных - уникальный код товара
- UUID
Плохой вариант — индекс в массиве:
<!-- Так делать нежелательно -->
<li v-for="(user, index) in users" :key="index">
{{ user.name }}
</li>
Использование индекса ломает основную идею ключа: при вставке в середину массива индексы всех последующих элементов меняются, и Vue будет считать их новыми, хотя данные те же. Это может привести к:
- неправильному состоянию внутренних компонентов
- совпадению значений в инпутах не с теми объектами
- лишним перерисовкам
Индекс можно использовать только в редких случаях, когда:
- список никогда не меняет порядок
- элементы не вставляются и не удаляются в середине
- каждый элемент — «простой» и не содержит собственного состояния
key на уровне компонента
Часто вы рендерите не примитивные теги, а компоненты. Смотрите, как это выглядит:
<!-- Здесь мы выводим список компонент UserCard -->
<UserCard
v-for="user in users"
:key="user.id"
:user="user"
/>
// Компонент, который отвечает за отображение одного пользователя
export default {
name: 'UserCard',
props: {
user: {
type: Object,
required: true
}
}
}
Ключ все равно ставится на уровне v-for, даже если вы рендерите компонент. Это позволяет Vue не путать экземпляры компонентов и корректно обновлять их состояние.
Перебор объектов с помощью v-for
Базовый перебор объекта
Директива v-for умеет перебирать не только массивы, но и объекты. Здесь я размещаю пример, чтобы вам было проще понять:
<ul>
<!-- value — значение свойства, key — его имя -->
<li v-for="(value, key) in user">
{{ key }} — {{ value }}
</li>
</ul>
export default {
data() {
return {
user: {
name: 'Иван',
age: 30,
city: 'Москва'
}
}
}
}
Комментарии:
value— значение свойства ('Иван',30,'Москва')key— имя свойства ('name','age','city')
Добавление индекса при переборе объекта
Иногда бывает полезно получить еще и индекс свойства:
<ul>
<!-- index здесь — порядковый номер свойства при обходе объекта -->
<li v-for="(value, key, index) in user">
{{ index }}. {{ key }} — {{ value }}
</li>
</ul>
Важно понимать: порядок свойств в объекте в JavaScript определяется правилами движка, и в старых браузерах он может быть непредсказуемым. В современных реализациях он более стабилен, но все равно не стоит полагаться на него так же уверенно, как на порядок в массиве.
Когда лучше перейти к массиву
Если вы:
- хотите управлять порядком элементов
- часто добавляете или удаляете свойства
- сортируете эти данные
то удобнее будет хранить данные в массиве объектов, а не в «плоском» объекте. Тогда v-for будет работать предсказуемее, а вы сможете легко сортировать и фильтровать список.
Перебор диапазонов чисел
Vue поддерживает короткий синтаксис для генерации чисел от 1 до N. Это полезно, например, чтобы вывести заглушки, рейтинги или колонки таблиц.
Теперь вы увидите, как это выглядит в коде:
<ul>
<!-- Здесь мы генерируем числа от 1 до 5 -->
<li v-for="n in 5" :key="n">
Элемент номер {{ n }}
</li>
</ul>
Особенности:
n in 5— это синтаксис для числового диапазона- значения начинаются с 1, а не с 0
- ключом удобно использовать само число
n
Если вам нужен диапазон с 0 или нестандартным стартом/шагом, лучше использовать вычисляемое свойство и массив:
export default {
computed: {
// Здесь мы создаем массив чисел от 0 до 4
numbers() {
return Array.from({ length: 5 }, (_, index) => index)
}
}
}
<!-- Теперь перебираем массив numbers -->
<div v-for="n in numbers" :key="n">
Число {{ n }}
</div>
Так вы полностью контролируете значения и можете задать любой шаг или фильтрацию.
v-for и вычисляемые данные
Перебор вычисляемых списков
Часто список, который вы рендерите, — это не «сырые» данные, а результат фильтрации или сортировки. В Vue удобнее всего это делать через вычисляемые свойства (computed).
Давайте посмотрим, что происходит в следующем примере:
export default {
data() {
return {
// Исходный список задач
tasks: [
{ id: 1, title: 'Купить продукты', done: false },
{ id: 2, title: 'Сделать отчет', done: true },
{ id: 3, title: 'Позвонить клиенту', done: false }
],
showDone: false // Флаг, по которому мы фильтруем список
}
},
computed: {
// Здесь мы возвращаем отфильтрованный список
filteredTasks() {
if (this.showDone) {
// Показываем только выполненные задачи
return this.tasks.filter(task => task.done)
}
// Показываем только невыполненные задачи
return this.tasks.filter(task => !task.done)
}
}
}
<ul>
<!-- Перебираем вычисляемое свойство filteredTasks -->
<li v-for="task in filteredTasks" :key="task.id">
{{ task.title }}
</li>
</ul>
Комментарии:
- логика фильтрации и сортировки сосредоточена в computed
- шаблон остается простым и читаемым
- Vue кэширует вычисляемое свойство, поэтому оно пересчитывается только при изменении зависимостей
Почему не стоит писать сложные выражения прямо в v-for
Формально вы можете написать так:
<!-- Пример, который лучше не использовать -->
<li v-for="task in tasks.filter(t => !t.done)" :key="task.id">
{{ task.title }}
</li>
Проблема:
- при каждом рендере будет заново выполняться фильтрация
- этот код труднее тестировать и переиспользовать
- логика «размазана» между шаблоном и скриптом
Гораздо удобнее вынести это в вычисляемое свойство и обращаться к нему из шаблона, как мы сделали в предыдущем примере.
v-for и v-if вместе
Почему нужно быть осторожным
Иногда вам хочется одновременно перебирать список и фильтровать элементы сразу в шаблоне:
<!-- Пример, который чаще всего стоит избегать -->
<li v-for="task in tasks" v-if="!task.done" :key="task.id">
{{ task.title }}
</li>
Проблема здесь в том, что:
- v-for имеет более высокий приоритет, чем v-if
- сначала Vue создаст виртуальные элементы для всех задач
- затем отфильтрует их по условию v-if
Это:
- менее эффективно по производительности
- делает код менее очевидным
- может привести к неожиданным эффектам при использовании key
Рекомендуемый подход через computed
Лучше фильтровать данные заранее, а не внутри v-for. Смотрите, я покажу вам переписанную версию:
export default {
data() {
return {
tasks: [
{ id: 1, title: 'Купить продукты', done: false },
{ id: 2, title: 'Сделать отчет', done: true }
]
}
},
computed: {
// Здесь мы заранее отбираем только невыполненные задачи
undoneTasks() {
return this.tasks.filter(task => !task.done)
}
}
}
<ul>
<!-- Здесь v-for работает уже по отфильтрованному списку -->
<li v-for="task in undoneTasks" :key="task.id">
{{ task.title }}
</li>
</ul>
Так код становится и чище, и эффективнее.
Исключения
Иногда v-if в одном элементе с v-for допустим, например, если условие не связано с текущим элементом, а относится ко всему списку:
<ul v-if="tasks.length">
<!-- Список выводим только если в нем есть элементы -->
<li v-for="task in tasks" :key="task.id">
{{ task.title }}
</li>
</ul>
<p v-else>
Задач нет
</p>
Здесь условие v-if="tasks.length" не зависит от task, так что порядок применения директив не вызывает проблем.
Вложенные v-for
Базовый пример: список категорий и товаров
Вложенные циклы позволяют строить более сложные структуры. Например, у вас есть категории и товары внутри каждой категории.
Покажу вам, как это реализовано на практике:
<div v-for="category in categories" :key="category.id">
<h3>{{ category.title }}</h3>
<ul>
<!-- Внутренний v-for для товаров внутри одной категории -->
<li
v-for="product in category.products"
:key="product.id"
>
{{ product.name }} — {{ product.price }} ₽
</li>
</ul>
</div>
export default {
data() {
return {
categories: [
{
id: 1,
title: 'Фрукты',
products: [
{ id: 101, name: 'Яблоко', price: 50 },
{ id: 102, name: 'Банан', price: 60 }
]
},
{
id: 2,
title: 'Овощи',
products: [
{ id: 201, name: 'Морковь', price: 30 },
{ id: 202, name: 'Огурец', price: 40 }
]
}
]
}
}
}
Комментарии:
- у внешнего и внутреннего списка должны быть свои key
- в каждом v-for ключ уникален в рамках текущего набора
- лучше использовать разные имена переменных (
category,product), чтобы не путаться
Доступ к индексам во вложенных циклах
Если вам нужно использовать индексы в обоих уровнях, вы можете сделать так:
<div
v-for="(category, categoryIndex) in categories"
:key="category.id"
>
<h3>{{ categoryIndex + 1 }}. {{ category.title }}</h3>
<ul>
<li
v-for="(product, productIndex) in category.products"
:key="product.id"
>
{{ categoryIndex + 1 }}.{{ productIndex + 1 }}
— {{ product.name }}
</li>
</ul>
</div>
Будьте внимательны с индексами, когда меняете порядок элементов или фильтруете их: номера могут меняться, и это нормально. Это еще одна причина, почему индексы не стоит использовать в качестве key.
v-for и шаблоны без обертки
Использование тега template
Иногда вам нужно обернуть несколько тегов под одним v-for, но при этом вы не хотите добавлять лишний элемент в DOM (например, лишний div). Для этого в Vue есть специальный тег template.
Давайте разберемся на примере:
<ul>
<!-- Здесь мы используем template, чтобы повторить сразу два тега -->
<template v-for="user in users" :key="user.id">
<!-- Этот li будет повторяться для каждого user -->
<li>
{{ user.name }}
</li>
<!-- И этот li тоже будет повторяться для каждого user -->
<li class="divider">
<!-- Разделитель между пользователями -->
---
</li>
</template>
</ul>
Комментарии:
- тег template не попадает в итоговый HTML
- все дочерние элементы template будут повторяться для каждого элемента массива
- key нужно указывать на template, если вы используете его как «корень» в v-for
Ограничение: один key на группу
Важно понимать: когда вы используете template с v-for, ключ применяется к группе элементов целиком. То есть все элементы, которые возвращает один проход цикла, считаются одним «узлом» с точки зрения диффинга Vue.
Это удобно для структур, в которых вам нужно логически объединить несколько тегов под одной итерацией.
v-for и компоненты
Передача данных через props
Чаще всего v-for используется для рендера дочерних компонентов. Например, список карточек:
<!-- Родительский шаблон -->
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
/>
// Компонент ProductCard
export default {
name: 'ProductCard',
props: {
product: {
type: Object,
required: true
}
}
}
Комментарии:
- каждый компонент ProductCard получает свой объект product
- ключ задается на уровне v-for
- внутри компонента вы можете спокойно использовать product в шаблоне и логике
Динамический список компонентов
Иногда тип компонента зависит от данных. Например, в ленте у вас могут быть «карточки статьи», «картинки», «баннеры» и т.д. Тогда вы можете рендерить динамический компонент:
<!-- Здесь мы используем компонент component с dynamic binding -->
<component
v-for="block in blocks"
:key="block.id"
:is="block.type"
:data="block.data"
/>
export default {
data() {
return {
// Каждый блок содержит тип компонента и его данные
blocks: [
{ id: 1, type: 'ArticleBlock', data: { title: '...' } },
{ id: 2, type: 'ImageBlock', data: { src: '...' } }
]
}
}
}
Так вы можете создавать гибкие страницы, состоящие из разных типов блоков, не дублируя логику.
Производительность и большие списки
Когда v-for может стать проблемой
Перебор двадцати или даже сотен элементов обычно не вызывает проблем. Но если вы рендерите:
- тысячи строк таблицы
- сложные компоненты с большим количеством логики
- списки, которые часто обновляются
то производительность может просесть.
Основные причины:
- большое количество DOM-узлов
- частые обновления данных
- тяжелая логика внутри каждого элемента
Практические рекомендации
Обратите внимание, как этот фрагмент кода решает задачу оптимизации:
Фильтруйте и сортируйте в computed, а не в шаблоне
Так вы избегаете лишних перерасчетов и делаете код проще для поддержки.
Используйте правильный key
Устойчивые ключи помогают Vue минимизировать количество изменений в DOM.
Разбивайте большие списки на страницы
Вместо 5000 элементов на одной странице показывайте 50–100 и добавляйте пагинацию.
<!-- Здесь мы показываем только текущую страницу списка --> <RowItem v-for="row in paginatedRows" :key="row.id" :row="row" />Рассмотрите виртуальный скролл
Для очень больших списков (десятки тысяч элементов) можно использовать библиотеки виртуального скролла, которые отображают только видимую часть списка. Например, такие компоненты есть в экосистеме Vue.
Избегайте тяжелых вычислений внутри каждого элемента
Если вы считаете что-то сложное для каждого элемента, вынесите это в вычисляемые свойства или предварительную обработку на стороне сервера.
Частые ошибки при работе с v-for
Отсутствие key или использование неуникального ключа
Один из самых распространенных источников странных багов:
<!-- Ошибка — отсутствует key -->
<li v-for="user in users">
{{ user.name }}
</li>
<!-- Ошибка — одинаковый key для разных элементов -->
<li v-for="user in users" :key="user.name">
{{ user.name }}
</li>
Проблема:
- дублирующиеся или отсутствующие ключи могут приводить к «миграции» состояния между элементами
- в консоли Vue обычно предупреждает об этом, но баги проявляются не всегда очевидно
Как правильно:
- всегда использовать key
- выбирать поле, которое гарантированно уникально (id, код, uuid)
Мутация исходных данных в шаблоне
Иногда разработчики пытаются изменить данные прямо в шаблоне в рамках v-for. Например, через обработчики событий без явного метода.
Лучше всегда выносить изменение состояния в методы компонента:
<li v-for="user in users" :key="user.id">
{{ user.name }}
<button @click="removeUser(user.id)">
Удалить
</button>
</li>
export default {
methods: {
// Здесь мы удаляем пользователя по id
removeUser(id) {
this.users = this.users.filter(user => user.id !== id)
}
}
}
Так легче отлаживать логику и избегать неожиданных сайд-эффектов.
Использование v-for на одном элементе с v-model без ключа
Если вы используете v-model внутри v-for (например, форма с несколькими полями), отсутствие key почти гарантированно приведет к некорректному поведению при добавлении/удалении элементов.
Пример корректной записи:
<div v-for="user in users" :key="user.id">
<!-- Здесь каждое поле четко привязано к своему объекту -->
<input v-model="user.name" />
<input v-model="user.email" />
</div>
Ключ обеспечивает правильное соответствие между объектом в массиве и DOM-элементами, где пользователь вводит данные.
Заключение
Директива v-for — один из ключевых инструментов Vue.js для работы со списками. Она позволяет:
- перебирать массивы, объекты и диапазоны чисел
- гибко управлять структурой шаблона, включая вложенные циклы и компоненты
- эффективно обновлять DOM при изменении данных, если вы правильно используете key
Подход, при котором основная логика фильтрации, сортировки и подготовки данных вынесена в вычисляемые свойства или методы, а шаблон содержит только v-for с простыми выражениями, делает код:
- предсказуемым
- удобным для отладки
- простым для расширения
Если вы будете внимательно относиться к выбору ключей, избегать сложных выражений прямо в v-for и не смешивать его с v-if без необходимости, работа со списками в Vue станет гораздо надежнее и понятнее.
Частозадаваемые технические вопросы по теме статьи и ответы на них
Как изменить порядок элементов, рендерящихся через v-for, при сортировке массива?
Сортировку лучше выполнять в вычисляемом свойстве, а не в шаблоне.
computed: {
sortedUsers() {
// Здесь мы создаем копию массива users и сортируем ее по имени
return [...this.users].sort((a, b) => a.name.localeCompare(b.name))
}
}
<li v-for="user in sortedUsers" :key="user.id">
{{ user.name }}
</li>
Так вы не мутируете исходный массив и не связываете сортировку с рендерингом напрямую.
Как безопасно обновлять элемент массива, который выводится через v-for?
Лучше не менять элемент по индексу напрямую, а создавать новый массив:
methods: {
updateUserName(id, newName) {
// Здесь мы создаем новый массив users с обновленным элементом
this.users = this.users.map(user =>
user.id === id ? { ...user, name: newName } : user
)
}
}
Это улучшает предсказуемость, а Vue корректно отреагирует на замену массива.
Можно ли использовать v-for в сочетании с keep-alive для списка динамических компонентов?
Да, но важно, чтобы:
- вы использовали устойчивый key на динамическом компоненте
- тип компонента (атрибут is) и key вместе однозначно определяли экземпляр
<keep-alive>
<component
v-for="widget in widgets"
:key="widget.id"
:is="widget.type"
:data="widget.data"
/>
</keep-alive>
Так состояние каждого компонента будет кэшироваться корректно.
Как использовать v-for в слоте дочернего компонента?
Вы можете перебирать данные в родителе и передавать их в слот:
<!-- Родитель -->
<List>
<template #default="{ item }">
<!-- Здесь вы описываете, как рендерить один элемент -->
<span>{{ item.name }}</span>
</template>
</List>
<!-- Дочерний компонент List -->
<div v-for="item in items" :key="item.id">
<!-- Здесь мы передаем item в слот -->
<slot :item="item" />
</div>
Так вы разделяете ответственность за данные и отображение.
Как корректно использовать v-for внутри SVG?
Внутри SVG вы тоже можете использовать v-for, но нужно соблюдать правильные теги и пространства имен. В Vue достаточно обычного синтаксиса:
<svg width="200" height="50">
<!-- Здесь мы рисуем несколько кругов на основе массива points -->
<circle
v-for="point in points"
:key="point.id"
:cx="point.x"
:cy="point.y"
r="5"
/>
</svg>
Важно: атрибуты задаются через привязку, как у обычных HTML-элементов. Vue сам позаботится о том, чтобы использовать SVG-пространство имен.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев