🔥 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/codegen
  • compiler-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 или простых идентификаторов), что для многих привязок избавляет от поиска через прокси в контексте рендера.

vue Reactivity — куда компилятор передаёт эстафету vue