🔥 Vue Compiler Pipeline (чистая версия для повторения)
💡 Ключевая идея Vue 3
Vue компилирует
templateв функциюrender(), которая во время выполнения создаёт VNode, а рендерер превращает их в DOM через patch.
🧭 Полная цепочка
TEMPLATE (строка)
↓
PARSE
↓
Template AST
↓
TRANSFORM
↓
Оптимизированный AST (директивы разрешены + patch flags)
↓
CODEGEN
↓
render-функция (JS-код)
↓
ВЫПОЛНЕНИЕ В RUNTIME
↓
render(ctx)
↓
h() / createVNode()
↓
Дерево VNode
↓
patch()
↓
Реальный DOM⚠️ КЛЮЧЕВОЕ РАЗДЕЛЕНИЕ (самое важное, что нужно запомнить)
🟡 COMPILER (время сборки)
Выполняется до запуска приложения (обычно через плагин сборщика — @vue/compiler-sfc, vue-loader, unplugin-vue).
Что делает:
- parse → AST
- transform → оптимизирует AST
- codegen → генерирует JS-код
👉 Результат: строка JS (исходный код render-функции)
Vue поставляет три пакета компилятора, у каждого своя задача:
compiler-core— платформо-независимая логика parse/transform/codegencompiler-dom— добавляет DOM-специфичные трансформации (v-html,v-modelна инпутах, модификаторы событий,v-show)compiler-sfc— компилирует однофайловые компоненты.vue(разделяет<template>,<script>/<script setup>,<style>, обрабатывает scoped CSS и макросы<script setup>вродеdefineProps/defineEmits)
🔵 RUNTIME (время в браузере)
Выполняется во время работы приложения.
Что делает:
- вызывает
render() - вызывает
h()/createVNode() - создаёт VNode
- вызывает
patch()
👉 Результат: DOM
🧠 ВАЖНОЕ УТОЧНЕНИЕ (про h)
Частая ошибка:
❌ Неверно:
h()выполняется послеrender
✅ Верно:
h()выполняется внутриrender, в процессе его выполнения
render(ctx)
↓
h()
↓
VNode🔧 1. PARSE
👉 превращает строку шаблона в AST
<div>{{ msg }}</div>→
AST - дерево(Element / Text / Interpolation)Парсер — это небольшой конечный автомат, который проходит шаблон посимвольно и порождает узлы вроде ELEMENT, TEXT, INTERPOLATION, COMMENT и ATTRIBUTE/DIRECTIVE.
🔧 2. TRANSFORM (самый “умный” этап)
👉 превращает “HTML AST” в “AST, готовый к JS”
Что делает:
✔ v-if
→ условное выражение (condition ? vnode1 : vnode2)
✔ v-for
→ renderList()
✔ v-model
→ props + привязки событий (например, :value + @input, или :modelValue + @update:modelValue для компонентов)
✔ статичные узлы
→ hoisting
✔ patchFlags
→ маркеры динамического содержимого
💡 Ключевая идея transform:
он превращает “структуру шаблона” в “будущую структуру JS-кода”
Трансформации выполняются как набор плагинов node transform и directive transform (transformElement, transformText, vOn, vBind, vFor, vIf, …), каждый из которых обходит AST и мутирует/аннотирует узлы (концептуально похоже на пайплайн плагинов Babel).
🔧 3. CODEGEN
👉 AST → render-функция на JS
return function render(_ctx) {
return createElementVNode("div", null, _ctx.msg)
}⚡ Важная деталь
👉 на этом этапе VNode ещё не существует
Есть только:
JS-код, который позже создаст VNode
🔵 4. RUNTIME: render()
render(ctx)👉 выполняется как обычная JS-функция
🔥 5. h() / createVNode()
h("div", ctx.msg)👉 создаёт:
VNode = { type, props, children }h() — это просто тонкая, удобная для разработчика обёртка над createVNode(): она нормализует аргументы (children/props можно опустить или переставить местами), прежде чем напрямую вызвать createVNode.
💥 ВАЖНО
VNode создаётся ТОЛЬКО во время выполнения
🔷 6. PATCH (движок диффинга)
старый VNode
vs
новый VNode👉 результат:
- обновление текста
- изменение props
- добавление/удаление DOM-узлов
- перемещение узлов (v-for + key)
Алгоритм диффинга сравнивает детей двусторонним (голова/хвост) способом, а затем при необходимости использует алгоритм наибольшей возрастающей подпоследовательности (LIS), чтобы минимизировать перемещения DOM для списков с ключами — вот почему :key в v-for так важен.
🧠 Оптимизации (очень важно, стоит знать точно)
✔ Hoisting
const _hoisted_1 = createVNode(...)👉 статичные узлы не пересоздаются на каждом рендере — они создаются один раз, вне render-функции, и переиспользуются по ссылке.
✔ Patch Flags
👉 битовые флаги, прикреплённые к VNode, которые сообщают runtime, что именно может измениться, чтобы диффинг мог пропустить всё остальное:
| Флаг | Значение |
|---|---|
TEXT | динамический текстовый контент |
CLASS | динамическая привязка class |
STYLE | динамическая привязка style |
PROPS | динамические props (со статичным известным набором ключей) |
FULL_PROPS | динамические props с динамическими ключами — нужен полный дифф |
HYDRATE_EVENTS | есть обработчики событий, требующие гидрации |
STABLE_FRAGMENT | фрагмент, порядок детей которого не меняется |
KEYED_FRAGMENT | фрагмент с детьми по ключам (например, v-for с :key) |
UNKEYED_FRAGMENT | фрагмент с детьми без ключей |
NEED_PATCH | нужны патчи, не связанные с props (например, refs, хуки) |
DYNAMIC_SLOTS | содержимое слотов может динамически меняться |
BAIL | оптимизация не сработала — нужен полный дифф |
👉 они позволяют runtime пропускать те части диффа, которые заведомо не могли измениться.
✔ Block Tree
👉 runtime отслеживает только dynamicChildren
не всё дерево DOM целиком,
только "то, что вообще может измениться"Block — это VNode, который во время создания собирает все свои динамические VNode-потомки в плоский массив dynamicChildren (openBlock() / closeBlock()). Во время patch Vue напрямую итерирует этот плоский массив вместо рекурсивного обхода всего дерева — именно это лежит в основе ускорения Vue 3 по сравнению с полным диффингом виртуального DOM в Vue 2.
⚠️ ВАЖНОЕ УТОЧНЕНИЕ ПРО dynamicChildren
👉 Vue не “игнорирует статичное дерево при диффинге”
На самом деле:
статичные узлы вообще не сравниваются — они полностью пропускаются, а не просто дёшево диффятся
🔥 Короткая формула Vue
template
→ AST
→ оптимизированный AST
→ render-функция
→ дерево VNode
→ patch
→ DOM🧠 Самая важная мысль (суть Vue)
Vue = 2 мира
🟡 Compiler
“Я превращаю шаблон в код”
🔵 Runtime
“Я выполняю код и обновляю UI”
💥 Финальная мысль
VNode — это не промежуточный шаг между компилятором и DOM — VNode и есть формат runtime для описания UI.
📎 Дополнительно: Vue 2 vs Vue 3 компилятор, вкратце
- Диффинг виртуального DOM в Vue 2 при каждом обновлении обходит и сравнивает всё дерево целиком — на этапе компиляции нет знания о том, что статично, а что динамично.
- Компилятор Vue 3 заранее делает статический анализ (hoisting + patch flags + block tree), поэтому дифф в runtime трогает только те узлы, которые заведомо могут измениться. Именно поэтому Vue 3 может быть быстрее Vue 2, хотя оба используют виртуальный DOM.
<script setup>(компилируетсяcompiler-sfc) дополнительно позволяет компилятору шаблонов разрешать привязки на этапе компиляции (инлайня их как_ctx.fooвместо$setup.fooили простых идентификаторов), что для многих привязок избавляет от поиска через прокси в контексте рендера.