← Курс/nextTick: работа с DOM после обновления#212 из 257+25 XP

nextTick: работа с DOM после обновления

Почему DOM не обновляется мгновенно

Vue не обновляет DOM синхронно при каждом изменении данных. Вместо этого он **буферизует все изменения** и применяет их разом в конце текущего «тика» (микрозадачи). Это называется **асинхронным обновлением DOM**.

// В Vue:
count.value = 1
count.value = 2
count.value = 3
// DOM обновится один раз со значением 3, а не три раза

Такой подход эффективнее — не нужно перерисовывать DOM при каждом маленьком изменении.

Проблема: чтение DOM сразу после изменения данных

Если вы попытаетесь прочитать DOM-элемент сразу после изменения данных, вы увидите старое значение:

<template>
  <div ref="containerEl">
    <p v-for="item in items" :key="item">{{ item }}</p>
  </div>
  <button @click="addItem">Добавить</button>
</template>

<script setup>
const items = ref(['первый'])
const containerEl = ref(null)

function addItem() {
  items.value.push('новый')
  // ПРОБЛЕМА: DOM ещё не обновился!
  console.log(containerEl.value.children.length) // всё ещё 1, не 2
}
</script>

Решение: nextTick()

Функция nextTick() возвращает Promise, который разрешается **после того, как Vue обновит DOM**:

<script setup>
import { ref, nextTick } from 'vue'

const items = ref(['первый'])
const containerEl = ref(null)

async function addItem() {
  items.value.push('новый')

  await nextTick()  // ждём обновления DOM

  // Теперь DOM актуален
  console.log(containerEl.value.children.length) // 2
}
</script>

Вариант с колбэком

Старый вариант — передать функцию напрямую:

nextTick(() => {
  // Выполнится после обновления DOM
  inputEl.value.scrollIntoView()
})

Практические сценарии

Фокус на динамически добавленный элемент

<script setup>
const showInput = ref(false)
const inputEl = ref(null)

async function toggleInput() {
  showInput.value = true
  await nextTick()
  inputEl.value.focus()  // фокус только когда input уже в DOM
}
</script>

Прокрутка к новому сообщению в чате

async function sendMessage(text) {
  messages.value.push({ id: Date.now(), text })
  await nextTick()
  // Прокручиваем к последнему сообщению
  chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}

Получение размеров после рендера

async function expandSection() {
  isExpanded.value = true
  await nextTick()
  const height = sectionEl.value.offsetHeight
  console.log('Высота раскрытой секции:', height)
}

Как это работает внутри

nextTick использует Promise.resolve().then() (микрозадачу) или MutationObserver. Vue накапливает все изменения, затем применяет их перед следующим тиком микрозадач. Когда Promise из nextTick разрешается, DOM уже гарантированно обновлён.

Примеры

Эмуляция очереди обновлений Vue и nextTick через Promise и микрозадачи

// Эмулируем механизм nextTick Vue: буферизация обновлений + flush через микрозадачу

// Очередь отложенных обновлений
let updateQueue = []
let isFlushing = false
let pendingPromise = null

// Псевдо-DOM (хранит актуальное состояние)
const pseudoDOM = {
  items: [],
  render(items) {
    this.items = [...items]
    console.log(`  [DOM] Перерисован. Элементов: ${this.items.length}. Содержимое: [${this.items.join(', ')}]`)
  }
}

// Планировщик: накапливает обновления и сбрасывает их микрозадачей
function queueUpdate(updateFn) {
  updateQueue.push(updateFn)
  console.log(`  [queue] Добавлено обновление. В очереди: ${updateQueue.length}`)

  if (!isFlushing) {
    isFlushing = true
    pendingPromise = Promise.resolve().then(flushQueue)
  }
}

function flushQueue() {
  console.log(`\n  [flush] Применяем ${updateQueue.length} обновлений к DOM...`)
  // В реальном Vue здесь происходит re-render компонентов
  updateQueue.forEach(fn => fn())
  updateQueue = []
  isFlushing = false
  pendingPromise = null
  console.log('  [flush] DOM обновлён.')
}

// nextTick: выполнить после обновления DOM
function nextTick(callback) {
  const p = pendingPromise
    ? pendingPromise.then(callback)           // дождаться текущего flush
    : Promise.resolve().then(callback)        // DOM уже актуален — следующая микрозадача

  return p
}

// --- Симуляция работы компонента ---

const reactiveItems = ['один']

async function main() {
  console.log('=== Сценарий 1: Чтение DOM до nextTick ===')

  // Добавляем элемент (как items.value.push('два') в Vue)
  reactiveItems.push('два')
  queueUpdate(() => pseudoDOM.render(reactiveItems))

  console.log('  [sync] DOM сейчас (до nextTick):', pseudoDOM.items)
  // Старое значение — DOM ещё не обновлён

  await nextTick(() => {
    console.log('  [nextTick] DOM после обновления:', pseudoDOM.items)
  })

  console.log('\n=== Сценарий 2: Несколько изменений — один ре-рендер ===')

  reactiveItems.push('три')
  queueUpdate(() => console.log('  [render] Первое изменение обработано'))

  reactiveItems.push('четыре')
  queueUpdate(() => console.log('  [render] Второе изменение обработано'))

  reactiveItems.push('пять')
  queueUpdate(() => {
    pseudoDOM.render(reactiveItems)
    console.log('  [render] Финальный рендер с ТРЕМЯ изменениями сразу')
  })

  console.log('  [sync] Добавили 3 элемента, но flush ещё не произошёл')

  await nextTick()
  console.log('  [nextTick] Все три изменения применены за один проход')

  console.log('\n=== Итог ===')
  console.log('Vue буферизует изменения и применяет их все разом.')
  console.log('nextTick() позволяет дождаться этого момента.')
}

main()