Статьи серии
- Новые возможности Vue 3.1 — 3.2
- Новые возможности Vue 3.3
С момента выхода Vue 3 было уже несколько интересных обновлений. В этой и последующих статьях я соберу самые важные из них.
Секция <script setup>
До Vue 3.2 уже существовал Composition API и функция setup(). Но многие разработчики жаловались, что код стал слишком многословным. И действительно, компонент выглядел примерно так:
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: {
title: String
},
setup(props) {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
})
</script>
<template>
<button @click="increment">
{{ title }} {{ count }}
</button>
</template>
Здесь содержится много служебного кода:
defineComponent();- функция
setup(); - обязательный
return, в котором нужно перечислить всё, что используется в шаблоне.
Свойства в этом случае приходилось описывать через параметр props.
После появления <script setup> то же самое можно записать так:
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
title: string
}>()
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ props.title }} {{ count }}
</button>
</template>
Что изменилось:
- нет
defineComponent(); - нет функции
setup(); - нет
return; все переменные, функции и импорты верхнего уровня автоматически доступны в шаблоне
<script setup> — это синтаксический сахар. При сборке, Vue компилятор автоматически преобразует этот блок в обычную функцию setup().
Но кроме уменьшения объема кода, есть и практические плюсы:
- немного лучшая производительность (технические детали: шаблон компилируется в функцию рендеринга в той же области видимости, без промежуточного прокси);
- лучшая поддержка TypeScript (при декларировании свойств и событий);
- проще читать компонент, так как меньше вложенность.
Именно поэтому сегодня <script setup> считается рекомендуемым способом написания компонентов на Composition API.
Макросы defineProps() и defineEmits()
Конструкции defineProps() и defineEmits() — это еще одно важное нововведение Vue 3.2. Они появились именно благодаря <script setup> и являются макросами компилятора, а не обычными функциями. Почему они называются макросами? Потому что обрабатываются во время сборки. В этот момент вызовы преобразуется в обычное описание компонента.
Ранее, чтобы объявить свойства (props) и события компонента, нужно было использовать defineComponent():
<script lang="ts">
export default defineComponent({
props: {
title: String,
count: Number
},
emits: ['save', 'cancel'],
setup(props, { emit }) {
// ...
}
})
</script>
Начиная с Vue 3.2 это можно оформить более компактно:
<script setup lang="ts">
const props = defineProps<{
title: string
count: number
}>()
const emit = defineEmits<{
(e: 'save'): void
(e: 'cancel'): void
}>()
</script>
Так как это макросы, то
- их не нужно импортировать;
- нельзя вызвать внутри
if, цикла или обычной функции; - можно использовать только на верхнем уровне
<script setup>.
Описание свойств
Одно из главных преимуществ использования этих макросов — отличная работа с TypeScript.
По такому описанию свойств компонентов IDE сразу знает какие props существуют, какие обязательны и какого они типа:
const props = defineProps<{
title: string
count?: number
}>()
И может показывает автодополнения и ошибки.
Описание событий
Теперь можно описать сигнатуры:
const emit = defineEmits<{
(e: 'save', id: number): void
(e: 'cancel'): void
}>()
Теперь вызовы будут валидироваться:
emit('save', 123) // Всё в порядке
emit('save', '123') // Ошибка TypeScript
emit('unknown') // Ошибка
Выгоды от появления макросов следующие:
- они позволяют компилятору анализировать компонент еще на этапе сборки;
- дают гораздо более качественную поддержку TypeScript;
- делают описание интерфейса компонента.
Именно с появления defineProps() и defineEmits() сформировался современный стиль написания компонентов Vue 3, который сегодня используется практически во всех новых проектах. Позже эту идею продолжили другие макросы, такие как defineOptions(), defineSlots() и defineModel().
CSS v-bind()
CSS v-bind() — одно из самых интересных нововведений Vue 3.2. Оно позволяет использовать значения из JavaScript прямо в секции <style> компонента, не прибегая к инлайновым стилям.
Что было до Vue 3.2
Допустим, цвет кнопки зависит от свойства color. До появления v-bind() обычно писали так:
<template>
<button :style="{ color: textColor }">
Нажми меня
</button>
</template>
<script setup>
const textColor = 'red'
</script>
Или использовали CSS-переменные вручную:
<template>
<button :style="{ '--text-color': textColor }">
Нажми меня
</button>
</template>
<style>
button {
color: var(--text-color);
}
</style>
Это работало, но было довольно многословно.
После Vue 3.2
Теперь, после выхода Vue 3.2, можно написать:
<script setup>
const textColor = 'red'
</script>
<template>
<button>
Нажми меня
</button>
</template>
<style scoped>
button {
color: v-bind(textColor);
}
</style>
Как это работает
И снова магия сборки. На самом деле браузер не знает никакого v-bind() в CSS.
Во время сборки Vue преобразует код примерно в следующее:
button {
color: var(--v-123abc-textColor);
}
А при рендеринге компонента автоматически добавляет соответствующую CSS-переменную к корневому элементу компонента:
<div style="--v-123abc-textColor: red">
...
</div>
То есть v-bind() — это тоже компиляторная возможность, как и defineProps(). В итоговом CSS никакого v-bind() уже нет.
Допустимые значения
Можно использовать любые выражения.
Например:
width: v-bind(width);
background: v-bind(theme.primary);
font-size: v-bind(size + 'px');
Строковый вариант для сложных выражений:
color: v-bind('themes[theme].color');
background-image: v-bind('`url(${image})`');
Обратите внимание:
- внешние кавычки (
'...') — нужны Vue, чтобы воспринять всё содержимое как одну строку; - внутри строки находится полноценное JavaScript-выражение
Самое приятное свойство — всё остается реактивным.
effectScope()
effectScope() — это API, которое появилось в Vue 3.2 для группировки реактивных эффектов (watch, watchEffect, computed и т.д.) и их последующей совместной очистки.
Для большинства разработчиков это не повседневный инструмент. Он в первую очередь предназначен для авторов Composables, библиотек и сложной инфраструктуры.
Что было до Vue 3.2
Когда вы создаете watch() внутри компонента, Vue автоматически остановит его при уничтожении компонента.
watch(count, () => {
console.log(count.value)
})
Но если реактивные эффекты создаются вне компонента или вы хотите управлять их жизненным циклом самостоятельно, возникают проблемы.
Например:
watch(source1, ...)
watch(source2, ...)
watchEffect(...)
Чтобы остановить их, нужно хранить все функции очистки:
const stop1 = watch(...)
const stop2 = watch(...)
const stop3 = watchEffect(...)
// ...
stop1()
stop2()
stop3()
Если эффектов десятки, управлять ими становится неудобно.
Что дает effectScope()
Он позволяет создать область (scope), внутри которой будут зарегистрированы все реактивные эффекты.
const scope = effectScope()
scope.run(() => {
watch(...)
watch(...)
watchEffect(...)
})
Позже достаточно вызвать:
scope.stop()
И все эффекты, созданные внутри run(), будут остановлены.
Не нужно хранить множество функций stop().
Где это особенно полезно
Представьте Composable, который создает несколько наблюдателей. Без effectScope() ему приходится вручную отслеживать очистку. С effectScope() можно сгруппировать всё вместе и при необходимости отключить одной командой.
Как это связано с компонентами
Интересный факт: каждый компонент Vue уже имеет собственный effectScope.
Именно поэтому watch(), watchEffect() и computed() автоматически очищаются при размонтировании компонента. То есть effectScope() — это возможность создать такую же область вручную.
Когда он нужен обычному разработчику
В большинстве приложений — почти никогда. Если вы пишете обычные компоненты, достаточно обычного вызова watch. Vue сам всё очистит.
effectScope() становится полезен, если вы делаете что-то из этого списка:
- разрабатываете библиотеку;
- пишете сложные composables;
- создаете собственные менеджеры состояния;
- работаете с реактивностью вне жизненного цикла компонентов.
Многие возможности экосистемы и сторонних библиотек используют его «под капотом» для безопасного управления реактивностью и предотвращения утечек памяти.
defineCustomElement()
defineCustomElement() — это API, появившееся в Vue 3.2, которое позволяет превратить Vue-компонент в Web Component (пользовательский HTML-элемент).
Если раньше Vue-компоненты можно было использовать только внутри Vue-приложения, то теперь их можно публиковать как стандартные браузерные элементы.
Что такое Web Components
Web Components — это стандарт браузера, позволяющий создавать собственные HTML-теги.
Например:
<user-card></user-card>
<date-picker></date-picker>
<app-button></app-button>
Для браузера они работают так же, как <button> или <input>.
Главное преимущество — они не зависят от фреймворка. Такой компонент можно использовать в обычном HTML, React, Angular, Vue, Svelte или вообще без какого-либо фреймворка.
Что было до Vue 3.2
Допустим, есть компонент:
<UserCard />
Он мог работать только внутри Vue-приложения.
Если хотелось встроить его на сторонний сайт или в приложение на React, приходилось:
- либо переписывать компонент;
- либо подключать всё Vue-приложение целиком.
Что появилось в Vue 3.2
Теперь достаточно написать:
import { defineCustomElement } from 'vue'
import UserCard from './UserCard.ce.vue'
const UserCardElement = defineCustomElement(UserCard)
customElements.define('user-card', UserCardElement)
После этого в любом месте страницы можно использовать:
<user-card></user-card>
Браузер сам создаст экземпляр Vue-компонента.
Shadow DOM
По умолчанию Web Component использует Shadow DOM.
Это означает, что его стили изолированы. То есть стили страницы не влияют на компонент и стили компонента не «утекают» наружу. Это удобно для создания переиспользуемых UI-компонентов.
Когда это полезно
defineCustomElement() подходит, если вы хотите:
- создать библиотеку UI-компонентов для разных проектов;
- встроить Vue-компонент в сайт без Vue;
- постепенно мигрировать большое приложение на Vue;
- поставлять независимые виджеты (например, чат, форму обратной связи или платежный модуль).
Почему это не стало массовой практикой
Хотя идея выглядит очень привлекательно, большинство Vue-разработчиков почти не используют defineCustomElement(), потому что:
- если всё приложение написано на Vue, проще использовать обычные Vue-компоненты;
- Web Components имеют некоторые ограничения (например, есть нюансы с передачей сложных объектов через атрибуты, слотами и интеграцией с разными фреймворками);
- при использовании Vue-компонента как Web Component всё равно требуется рантайм Vue, поэтому это не дает превратить компонент в «чистый HTML».
Насколько это важное нововведение
Для обычной разработки на Vue это скорее нишевая возможность. Но для авторов библиотек и компаний, которые поддерживают приложения на разных технологиях, defineCustomElement() стал важным шагом: он позволил использовать Vue как инструмент для создания стандартных Web Components, которые можно встраивать практически в любую современную веб-платформу.
Директива v-memo
v-memo — это директива, появившаяся в Vue 3.2 для пропуска повторного рендеринга части шаблона, если определенные значения не изменились.
Ее можно сравнить с React.memo, но работает она на уровне шаблона.
Зачем она нужна
По умолчанию при изменении реактивного состояния Vue повторно выполняет функцию рендера компонента.
Например:
<template>
<h1>{{ title }}</h1>
<ExpensiveTable :items="items" />
</template>
Если изменится title, компонент будет заново отрендерен. Vue, конечно, очень эффективно обновляет DOM, но ему всё равно придется заново вычислить виртуальное дерево (Virtual DOM).
Если часть шаблона очень большая и зависит только от items, можно подсказать Vue, что ее не нужно пересчитывать без необходимости.
Как используется
<div v-memo="[items]">
...
</div>
Пока ссылка на items не изменилась, Vue повторно использует предыдущий результат рендера этой части шаблона.
Если items изменится — блок будет пересчитан.
Несколько зависимостей
Можно указать несколько значений:
<div v-memo="[items, selectedId]">
Блок будет обновляться только тогда, когда изменится хотя бы одно из них.
Чем отличается от v-once
Есть похожая директива:
<div v-once>
Но она означает:
Отрисовать один раз и никогда больше не обновлять.
v-memo гораздо гибче:
Обновлять только при изменении указанных зависимостей.
Типичный пример
Представим список из 10 000 элементов.
При выборе одного элемента:
<li
v-for="item in items"
:key="item.id"
>
...
</li>
Vue пройдет по всему списку, чтобы определить, что изменилось.
Можно написать:
<li
v-for="item in items"
:key="item.id"
v-memo="[item.id === selectedId]"
>
...
</li>
Теперь для большинства элементов условие item.id === selectedId останется прежним, и Vue сможет пропустить повторный рендер этих элементов. Фактически обновятся только два элемента: тот, который перестал быть выбранным, и тот, который стал выбранным.
Именно для таких сценариев v-memo и создавался.
Когда использовать
К сожалению, практически никогда.
Это может звучать неожиданно, но команда Vue прямо говорит, что v-memo — это инструмент для очень редких случаев. Большинство приложений не нуждается в нем, потому что:
- Vue уже эффективно оптимизирует рендеринг;
- компилятор автоматически помечает статические части шаблона;
- обновление DOM и так минимально.
v-memo имеет смысл только в случаях если:
- есть очень большие списки;
- рендеринг действительно стал узким местом;
- профилирование показало, что именно повторный рендер занимает заметное время.
Важный момент
v-memo не предотвращает изменение данных и не делает значения некэшируемыми. Он лишь говорит компилятору и рантайму Vue:
«Если зависимости не изменились, повторно используй уже вычисленный результат рендера этого участка шаблона».
То есть это оптимизация рендеринга, а не реактивности.
Итог по v-memo
v-memo — мощный, но узкоспециализированный инструмент. Для большинства компонентов его использование не принесет никакой пользы, а иногда даже усложнит код. Однако при работе с очень большими списками или дорогостоящими шаблонами он может дать ощутимый выигрыш в производительности.