Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке nexttick у Vue.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)`nextTick` у Vue.js запускає твій код після того, як DOM відобразив зміни реактивного стану. Vue групує оновлення DOM задля продуктивності: стан змінюється одразу, але DOM оновлюється в наступному мікротаску. Використовуй `nextTick`, коли потрібно зчитати DOM, сфокусувати елемент або прокрутити сторінку одразу після зміни стану. ```js await nextTick() console.log(el.textContent) // відображає актуальний стан ``` **Головне:** спочатку зміна стану, потім `nextTick`, потім робота з DOM.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`nextTick`** - утиліта Vue, яка запускає твій код після того, як DOM відобразив поточний реактивний стан. ## Теорія ### TL;DR - Vue нагадує кухню ресторану: замовлення (зміни стану) накопичуються і готуються пакетом, потім подаються всі разом; `nextTick` чекає, поки страви дійдуть до столу, перш ніж перевіряти зал - Стан змінюється миттєво; DOM оновлюється в наступному мікротаску - `nextTick` = мікротаск (до перемальовування браузером); `setTimeout(fn, 0)` = макротаск (після перемальовування) - Правило: якщо потрібно читати або змінювати DOM одразу після зміни стану, використовуй `nextTick` ### Швидкий приклад ```vue <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 до зміни стану** ```js await nextTick() // Черга порожня, виконується одразу state.value = newVal // Зміна відбувається пізніше ``` Коли `nextTick` запускається, немає жодних очікуваних оновлень. Він вирішується миттєво, і DOM все одно повертає старе значення. Зміна стану має бути першою. **2. Очікування nextTick у циклі** ```js for (let item of items) { state.value = item await nextTick() // Vue все одно пакетує всі зміни разом } ``` Vue пакетує оновлення між тіками. Очікування всередині циклу не дає одне DOM-оновлення на ітерацію - воно просто навантажує планувальник. Спочатку зроби всі зміни стану, потім один `nextTick` в кінці. **3. Подвійний nextTick (майже завжди зайвий)** ```js 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 після додавання елемента списку ```vue <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()` спрацює правильно. ### Автоматична прокрутка чату до останнього повідомлення ```vue <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` ще не включає нове повідомлення. Прокрутка щоразу зупиняється на передостанньому елементі. ### Вимірювання розмірів елемента після умовного рендеру ```vue <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` він змонтований і його можна вимірювати.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.