Рендер и DOM
Вопрос
Почему после смены данных DOM ещё старый, зачем :key не равен индексу и когда шаблона недостаточно?
Короткий ответ
Vue не переписывает DOM на каждую мутацию. Изменения копятся и применяются в следующем тике, через микрозадачу (Promise.resolve()). nextTick() — промис, который резолвится уже после этого обновления. Типичный случай: v-if показал инпут, фокус вешают только после await nextTick(), иначе элемента ещё нет.
:key в v-for — идентификатор узла для диффа. Индекс ломается при вставке, удалении и сортировке: Vue думает, что изменились данные существующих узлов, а не их порядок. Внутреннее состояние (текст в инпуте, анимация) остаётся на старой позиции.
Scoped slot отдаёт данные ребёнка родителю, а разметку решает родитель. <Teleport> рисует кусок шаблона в другом месте DOM, логика и события остаются у компонента. Render-функция h() нужна, когда структура тега сама зависит от данных и в шаблоне получится лесенка из v-if.
Разбор
Слот с данными:
<!-- Child.vue -->
<slot name="item" :data="itemData" />
<!-- Parent.vue -->
<Child>
<template #item="{ data }">
<p>{{ data.title }}</p>
</template>
</Child>Ребёнок владеет логикой (фильтр, пагинация, загрузка), родитель — внешним видом строки.
<Teleport to="body"> выносит модалку, тултип или тост из родителя с overflow: hidden, transform или своим z-index. Связь с состоянием Vue не рвётся: v-if и обработчики живут в том же компоненте.
Динамический заголовок через h, без шести веток в шаблоне:
<script setup>
import { h, useSlots } from 'vue'
const props = defineProps({ level: { type: Number, required: true } })
const slots = useSlots()
const DynamicHeading = () => h(`h${props.level}`, slots.default?.())
</script>
<template>
<DynamicHeading />
</template>Тот же приём используют обёртки над иконками и headless-компоненты, где слот — функция, а не статичная разметка.
На что смотрят на senior
nextTick не заменяет ожидание данных с сервера. Он ждёт только патч DOM, который Vue уже запланировал. Если нужен кадр после отрисовки браузером, ближе requestAnimationFrame, а не второй nextTick.
Ключ должен быть стабильным идентификатором сущности, не случайным числом на каждый рендер: новый ключ каждый раз уничтожает узел. Для списка без id ключ собирают из полей, которые не меняются при сортировке.
Teleport не изолирует стили и не создаёт новый корень приложения. События всплывают по Vue-дереву, даже если узел лежит в body. Портал не лечит фокус и ловушку клавиатуры в модалке — это отдельная работа.
Типичные ошибки
- Вызывать
input.focus()сразу после переключенияv-if, безnextTick. - Ставить
:key="index"на список с инпутами или transition. - Тащить модалку внутрь карточки с
overflow: hiddenи лечить это толькоz-index. - Писать render-функцию там, где хватает одного слота.
Задача
По клику visible становится true, сразу после этого код делает input.focus(). Фокуса нет, ошибки тоже. Что вставить между ними?
<input v-if="visible" ref="input" />Показать ответ
await nextTick(). Пока тик не прошёл, v-if ещё не вставил узел, ref пустой.
nextTick не ждёт сеть и не ждёт кадр браузера. Он ждёт патч DOM, который Vue уже поставил в очередь из-за смены visible.