Skip to Content
VueРендер и DOM

Рендер и DOM

Уровень: middle.

Вопрос

Почему после смены данных 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.

Last updated on