Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працює virtual DOM у Vue.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Virtual DOM у Vue.js** - дерево JS-об'єктів, яке відображає структуру реального DOM. Vue порівнює старе і нове дерево та змінює тільки ті вузли, які реально змінились. ```javascript // Структура VNode { type: 'p', props: {}, children: ['Лічильник: 5'] } // Vue дифає старий і новий VNode, патчить тільки текстовий вузол ``` Компілятор Vue перетворює `<template>` на функцію `render()`, яка повертає VNode-и. При реактивній зміні `patch(oldVNode, newVNode)` обходить обидва дерева і викликає браузерні API тільки там де є різниця. `patchFlags` від компілятора дозволяють пропускати незмінені піддерева. **Ключове:** батчинг через `nextTick()` означає що 10 мутацій стану дають 1 прохід патчингу, а не 10.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Virtual DOM у Vue.js** - це дерево JS-об'єктів, яке відображає структуру реального DOM. Vue використовує його, щоб обрахувати мінімальний набір змін у DOM після кожної зміни даних. ## Теорія ### TL;DR - Уявляй ескіз на папері перед тим як фарбувати стіни: перефарбовуєш тільки те, що змінилось. - Vue компілює `<template>` у функцію `render()`, яка повертає VNode-и: прості JS-об'єкти з полями `type`, `props`, `children`, `key`. - При реактивній зміні Vue запускає `patch(oldVNode, newVNode)` і викликає браузерні API тільки там, де є різниця. - Virtual DOM займає більше пам'яті (~2x від розміру DOM) і дає повільніший початковий рендер, але робить часті оновлення дешевшими. - Правило вибору: для інтерактивних UI з частими змінами стану - virtual DOM. Для статичних сторінок - не потрібен. ### Швидкий приклад ```vue <script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <p>Лічильник: {{ count }}</p> <!-- тут створюється VNode --> <button @click="count++">+1</button> </template> ``` Натисни кнопку: Vue створює нове дерево VNode-ів, порівнює його з попереднім і патчить тільки текстовий вузол всередині `<p>`. VNode кнопки ідентичний попередньому і пропускається повністю. ### Virtual DOM vs прямий DOM Прямий запис у DOM (`element.innerHTML = ...`) запускає повний reflow та repaint при кожному присвоєнні. Vue групує всі реактивні зміни в один прохід diff-у, обчислює точні мутації (наприклад, заміна `textContent`) і застосовує їх разом через `patch()`. Для сторінки з десятьма оновленнями на секунду ця різниця добре відчутна. ### Як Vue будує віртуальне дерево Компілятор Vue перетворює `<template>` на функцію `render()`. Вона повертає дерево VNode-ів - звичайні JS-об'єкти з полями `type`, `props`, `children` і `key`. Коли реактивний стан змінюється (спрацьовує сеттер `ref()`), планувальник Vue ставить ефект рендеру в чергу. На наступному тіку `render()` запускається знову, повертає нове дерево, і обидва дерева передаються у `patch()`. V8 оптимізує обхід цих об'єктів через приховані класи (hidden classes), тому сам diff виконується швидко. Браузерні `createElement()` і `setAttribute()` викликаються тільки там, де дерева відрізняються. ### Алгоритм патчингу `patch(oldVNode, newVNode)` рекурсивно обходить обидва дерева з кількома евристиками. Однаковий тег і однаковий `key`? Оновлюємо props і заходимо у дочірні вузли. Різний тег? Відмонтовуємо старий, монтуємо новий. Для списків сусідніх елементів Vue використовує алгоритм найдовшої зростаючої підпослідовності (longest increasing subsequence), щоб знайти мінімальну кількість переміщень. Саме тому `:key` такий важливий у `v-for`: без нього Vue порівнює за індексом, що дає хибні результати при вставках і видаленнях. Бітові маски `shapeFlag` (наприклад, `1=Element`, `4=Text`, `6=Fragment`) дозволяють Vue розгалужуватись за O(1) замість перевірок через `instanceof`. `patchFlags` йдуть ще далі: компілятор вставляє прапорці типу `TEXT=1` або `PROPS=16`, щоб рантайм пропускав повний рекурсивний обхід, коли змінився тільки текст. У продакшн-білдах це дає приблизно 90% прискорення порівняно з наївним повним diff-ом. ### Батчинг і nextTick Vue не патчить DOM після кожного окремого реактивного запису. Він ставить задачі в чергу і скидає їх у мікротаску через `Promise.resolve().then()`. До цього моменту і чіпляється `nextTick()`. Зроби десять мутацій стану поспіль - отримаєш один прохід патчингу, не десять. Бачив код, де `nextTick` викликали всередині `for`-циклу очікуючи послідовних DOM-читань - так не працює. Один flush на тік. ### Типові помилки **Помилка: `:key` на вузлах поза списком** ```vue <!-- Неправильно: при кожному count++ елемент знищується і створюється знову --> <div :key="count">Статичний текст</div> <!-- Правильно: для стабільних вузлів key не потрібен --> <div>Статичний текст</div> ``` Кожен `count++` знищує і відтворює елемент заново, втрачаючи фокус і ламаючи CSS-анімації. **Помилка: пряма мутація props у дочірньому компоненті** ```vue <script setup> const props = defineProps(['item']) props.item.done = true // Неправильно: обходить реактивність </script> ``` Vue перевіряє батьківський масив поверхнево. Пряма мутація не запускає реактивний сеттер, новий VNode не створюється і патч не відбувається. Правильно відправляти подію наверх: `emit('update:item', ...)`. **Помилка: індекс масиву як key** ```vue <!-- Неправильно: при вставці всі індекси зсуваються --> <li v-for="(todo, i) in todos" :key="i"> <!-- Правильно: стабільний унікальний id --> <li v-for="todo in todos" :key="todo.id"> ``` Вставиш елемент на початок списку - кожен індекс зсунеться на одиницю. Vue побачить що всі ключі змінились і перерендерує весь список, а не тільки новий елемент. **Помилка: неправильні очікування від Teleport** `<Teleport to="body">` переміщує VNode-и в іншу частину реального DOM, але diff все одно відбувається в дереві компонента. Якщо цільовий елемент не існує під час монтування компонента, Vue 3.2+ виводить попередження і Teleport не спрацьовує. Завжди переконуйся що target змонтований раніше. ### Де зустрічається - Vue/Nuxt TodoMVC: `v-for :key` дифає 1000+ елементів на 60fps без видимих затримок. - Quasar (динамічні форми): умовні VNode-и через `v-if` не рендерять невикористані поля взагалі. - Element Plus (таблиці): ключовані рядки дозволяють переупорядковувати колонки за O(n) замість перебудови всієї таблиці. - Pinia-керовані списки: пакетні мутації стору скидаються як один патч, не по одному. ### Питання на співбесіді **Q:** Як Vue дифає сусідні VNode-и без ключів? **A:** Порівнює за індексом і порядком. Вставка на позицію 0 змушує патчити всі наступні вузли без потреби. Саме тому існує ключований diffing. **Q:** Що таке `patchFlags` і навіщо вони потрібні? **A:** Компілятор вставляє цілочисельні прапорці в VNode-и під час збірки. Рантайм перевіряє прапорець перед тим як вирішити що дифати. `TEXT=1` означає що змінився тільки текст, тому Vue пропускає порівняння props. У реальних застосунках це скорочує час diff-у приблизно на 90% порівняно з повним рекурсивним обходом. **Q:** Чому `nextTick()` повертає Promise? **A:** Vue скидає задачі патчингу в мікротаску. `nextTick()` повертає Promise, який розв'язується після цього скидання, тому можна `await nextTick()` і зчитувати оновлені DOM-значення без race conditions. **Q:** Як SSR обробляє virtual DOM? **A:** `renderToString()` серіалізує VNode-и в HTML-рядок. На сервері diff не запускається. Клієнт отримує цей HTML і гідратує його, патчачи вузли з сервера замість того щоб їх замінювати. **Q:** Чим Vue diff відрізняється від React Fiber? **A:** Vue запускає синхронний однопрохідний diff. React Fiber можна призупинити - він зупиняється посеред reconciliation і передає виконання більш пріоритетній роботі. Підхід Vue простіший і швидший для більшості випадків; підхід React кращий для дуже великих дерев з потребою в планувальнику. ## Приклади ### Базовий: лічильник з одним реактивним вузлом ```vue <script setup> import { ref } from 'vue' const count = ref(0) </script> <template> <p>Лічильник: {{ count }}</p> <button @click="count++">+1</button> </template> ``` Після кожного кліку Vue створює новий VNode для `<p>`. Diff знаходить що змінився тільки текстовий вузол і викликає `node.textContent = newCount`. VNode кнопки ідентичний попередньому і пропускається повністю. ### Середній: список задач з ключовим diffing-ом ```vue <script setup> import { ref, computed } from 'vue' const todos = ref([ { id: 1, text: 'Купити молоко', done: false }, { id: 2, text: 'Написати код', done: true } ]) const activeCount = computed(() => todos.value.filter(t => !t.done).length) const toggle = (id) => { const todo = todos.value.find(t => t.id === id) todo.done = !todo.done // Запускає diff на списку } </script> <template> <ul> <li v-for="todo in todos" :key="todo.id" @click="toggle(todo.id)"> {{ todo.text }} <span v-if="todo.done">(виконано)</span> </li> </ul> <p>{{ activeCount }} активних</p> </template> ``` Клік на задачу: Vue дифає список за `key`, патчить тільки натиснутий `<li>` (додає або прибирає `<span>`) і перераховує VNode `activeCount`. Решта елементів списку не зачіпається. Без `:key="todo.id"` Vue перерендерував би всі `<li>` при будь-якій зміні масиву. ### Просунутий: Teleport і патчинг через межі DOM ```vue <script setup> import { ref } from 'vue' const showModal = ref(false) </script> <template> <button @click="showModal = true">Відкрити</button> <Teleport to="body"> <div v-if="showModal" class="modal" @click="showModal = false"> Натисни щоб закрити </div> </Teleport> </template> ``` Коли `showModal` стає `true`, Vue патчить вузол `<body>` напряму через прапорець Teleport VNode, а не корінь компонента. Diff все одно відбувається в порядку дерева компонентів, але DOM-мутація потрапляє в зовсім інший вузол. Якщо `body` недоступний під час монтування компонента (наприклад, при SSR), Vue 3.2+ виводить попередження і Teleport не спрацьовує. Це найчіткіший приклад того, що позиції у virtual DOM і реальному DOM не обов'язково збігаються.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.