Що таке nexttick у Vue.js?
nextTick - утиліта Vue, яка запускає твій код після того, як DOM відобразив поточний реактивний стан.
Теорія
TL;DR
- Vue нагадує кухню ресторану: замовлення (зміни стану) накопичуються і готуються пакетом, потім подаються всі разом;
nextTickчекає, поки страви дійдуть до столу, перш ніж перевіряти зал - Стан змінюється миттєво; DOM оновлюється в наступному мікротаску
nextTick= мікротаск (до перемальовування браузером);setTimeout(fn, 0)= макротаск (після перемальовування)- Правило: якщо потрібно читати або змінювати DOM одразу після зміни стану, використовуй
nextTick
Швидкий приклад
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const increment = async () => {
count.value++
console.log(count.value) // ✅ 1 - реактивний стан вже змінився
// DOM тут ще показує "Count: 0"
await nextTick()
console.log($refs.msg.textContent) // ✅ "Count: 1" - DOM наздогнав
}
</script>
<template>
<p ref="msg">Count: {{ count }}</p>
<button @click="increment">+1</button>
</template>Без nextTick читання $refs.msg.textContent одразу після count.value++ повертає застарілі дані. Реактивне значення вже готове, але DOM відстає на один тік.
Чому Vue відкладає оновлення DOM
Vue використовує Proxy для відстеження залежностей під час рендеру. Коли стан змінюється, Vue не перерендерює компонент одразу. Замість цього він ставить оновлення в чергу планувальника, а потім у наступному мікротаску (через Promise.resolve().then()) обробляє всю чергу і оновлює DOM за один прохід.
Логіка проста. Якщо в одній функції змінити п'ять реактивних значень, DOM оновиться один раз, а не п'ять. Один reflow, одне перемальовування, стабільні 60 кадрів на секунду.
nextTick підключається до тієї ж черги. Коли ти await nextTick(), ти чекаєш завершення цього проходу. Після нього DOM гарантовано актуальний.
Коли використовувати
- Виміряти елемент після зміни стану -
await nextTick(), потімgetBoundingClientRect() - Сфокусувати input після умовного рендеру - перемкнути
v-if, потімawait nextTick(), потім.focus() - Прокрутити до нового елемента списку - додати в масив, потім
await nextTick(), потім.scrollIntoView() - Ініціалізувати сторонню DOM-бібліотеку - встановити дані, потім
await nextTick(), потімnew Chart(el) - Запустити анімацію після переключення - змінити флаг, потім
await nextTick(), потім.classList.add('animate')
Патерн завжди однаковий: спочатку зміна стану, потім nextTick, потім робота з DOM.
Як це працює всередині
У Vue 3 планувальник живе в @vue/runtime-core. Коли спрацьовує watcher або computed, він додає завдання в глобальний масив-чергу. Vue потім викликає queueMicrotask() (або Promise.resolve().then() як fallback) щоб запланувати flushSchedulerQueue(). Ця функція запускає patch() для кожного компонента в черзі і мутує реальний DOM.
nextTick або приєднується до цього ж проходу (якщо він ще не завершився), або ставить колбек відразу після нього. Тому він точний: не гадає за таймером, а буквально стоїть наступним у черзі мікротасків.
Для порівняння: setTimeout(fn, 0) - це макротаск. Він запускається після того, як браузер вже перемалював сторінку. Ти можеш побачити миготіння старого вмісту до того, як твій код виконається. nextTick цього не допускає.
Типові помилки
1. Виклик nextTick до зміни стану
await nextTick() // Черга порожня, виконується одразу
state.value = newVal // Зміна відбувається пізнішеКоли nextTick запускається, немає жодних очікуваних оновлень. Він вирішується миттєво, і DOM все одно повертає старе значення. Зміна стану має бути першою.
2. Очікування nextTick у циклі
for (let item of items) {
state.value = item
await nextTick() // Vue все одно пакетує всі зміни разом
}Vue пакетує оновлення між тіками. Очікування всередині циклу не дає одне DOM-оновлення на ітерацію - воно просто навантажує планувальник. Спочатку зроби всі зміни стану, потім один nextTick в кінці.
3. Подвійний nextTick (майже завжди зайвий)
state.value = newVal
await nextTick() // DOM оновлено
await nextTick() // Черга вже порожня, виконується миттєвоДругий виклик вирішується одразу, бо черга порожня. Одного nextTick достатньо. Єдиний виняток: якщо колбек першого nextTick сам провокує нову реактивну зміну, що ставить у чергу другий прохід.
4. Використання nextTick в SSR
На сервері немає DOM. У середовищах server-side rendering, таких як Nuxt, nextTick стає no-op. Обгортай DOM-залежну логіку в onMounted разом з nextTick, щоб вона виконувалась тільки на клієнті.
Де зустрічається в реальних проектах
- Element Plus (
ElMessage) - позиціонує toast-сповіщення після вставки в DOM черезnextTick - Vuetify (
v-menu) - автоматично фокусує вміст меню після відкриття черезnextTick - Quasar (
QTable) - чекаєnextTickперед прокруткою до відсортованого рядка - Vue Test Utils / Vitest -
await nextTick()в тестах для синхронізації DOM після змін стану - Pinia плагіни - деякі плагіни чекають
nextTickдля DOM-залежних дій зі стором
Особисто я бачив патерн "фокус після рендеру" в майже кожному production Vue-додатку. Це одна з тих речей, що виглядає необов'язковою, поки не виявляєш, що в половини модальних вікон не працює автофокус.
Питання на співбесіді
Q: Яка різниця між nextTick і setTimeout(fn, 0)?
A: nextTick - мікротаск, виконується до перемальовування браузера. setTimeout(fn, 0) - макротаск, виконується після. Для читання DOM після зміни стану використовуй nextTick - він швидший і передбачуваніший.
Q: Чи працює nextTick в server-side rendering?
A: Ні. В SSR немає DOM, тому nextTick є no-op. Поміщай DOM-залежний код в onMounted і комбінуй з nextTick, якщо треба дочекатися першого рендеру.
Q: Яка різниця між синтаксисом callback і async/await?
A: nextTick(cb) реєструє одноразовий колбек. await nextTick() робить те саме, але дозволяє природно писати код нижче. Обидва варіанти робочі; await читається краще в сучасних async-функціях.
Q: Як nextTick пов'язаний з watchEffect?
A: watchEffect виконується синхронно при зміні реактивного значення, до оновлення DOM. Якщо потрібно читати DOM всередині watchEffect, обгортай це в nextTick. Інакше отримаєш значення до патчингу.
Q: Коли може знадобитись два послідовних nextTick?
A: Майже ніколи. Другий nextTick після першого запускається на порожній черзі. Єдиний edge case: якщо колбек першого nextTick сам провокує нову реактивну зміну, яка ставить у чергу новий прохід. Тоді другий nextTick чекатиме саме цього другого проходу.
Q: Як би ти реалізував власний планувальник з підтримкою пріоритетів, як у Vue?
A: Зберігай чергу об'єктів { job, priority }. При flush сортуй за пріоритетом від вищого до нижчого і запускай через queueMicrotask. Термінові завдання (читання DOM, фокус) отримують пріоритет 1, звичайні ефекти - 0. У Vue подібна концепція реалізована через pre-flush і post-flush watchers.
Приклади
Автофокус input після додавання елемента списку
<script setup>
import { ref, nextTick } from 'vue'
const todos = ref([])
const newTodo = ref('')
const inputRef = ref(null)
const addTodo = async () => {
if (!newTodo.value.trim()) return
todos.value.push({ id: Date.now(), text: newTodo.value })
newTodo.value = ''
await nextTick()
inputRef.value.focus() // Input вже є в DOM
inputRef.value.select() // Виділяємо текст, якщо є
}
</script>
<template>
<input ref="inputRef" v-model="newTodo" @keyup.enter="addTodo" placeholder="Додати завдання...">
<ul>
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</ul>
</template>Після todos.value.push(...) новий елемент списку ще не в DOM. await nextTick() чекає, поки Vue пропатчить DOM, і тоді .focus() спрацює правильно.
Автоматична прокрутка чату до останнього повідомлення
<script setup>
import { ref, nextTick } from 'vue'
const messages = ref([])
const chatRef = ref(null)
const sendMessage = async (text) => {
messages.value.push({ id: Date.now(), text })
await nextTick()
// scrollHeight відображає нове повідомлення тільки після патчингу DOM
chatRef.value?.scrollTo({
top: chatRef.value.scrollHeight,
behavior: 'smooth'
})
}
</script>
<template>
<div ref="chatRef" style="height: 300px; overflow-y: auto;">
<p v-for="msg in messages" :key="msg.id">{{ msg.text }}</p>
</div>
<button @click="sendMessage('Привіт!')">Надіслати</button>
</template>Без nextTick scrollHeight ще не включає нове повідомлення. Прокрутка щоразу зупиняється на передостанньому елементі.
Вимірювання розмірів елемента після умовного рендеру
<script setup>
import { ref, nextTick } from 'vue'
const visible = ref(false)
const boxRef = ref(null)
const boxSize = ref(null)
const toggle = async () => {
visible.value = !visible.value
if (visible.value) {
await nextTick()
// Елемент вже змонтований, getBoundingClientRect() повертає реальні значення
boxSize.value = boxRef.value?.getBoundingClientRect()
console.log('Ширина:', boxSize.value?.width)
}
}
</script>
<template>
<button @click="toggle">Перемкнути</button>
<div v-if="visible" ref="boxRef" style="width: 200px; height: 100px; background: steelblue;" />
<p v-if="boxSize">Ширина блоку: {{ boxSize.width }}px</p>
</template>До nextTick, при v-if="false", boxRef.value дорівнює null - елемента просто немає в DOM. Після nextTick він змонтований і його можна вимірювати.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.