Новые возможности Vue 3.1 — 3.2

This entry is part 1 of 2 in the series Обзор обновлений Vue 3

Статьи серии
  1. Новые возможности Vue 3.1 — 3.2
  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 — мощный, но узкоспециализированный инструмент. Для большинства компонентов его использование не принесет никакой пользы, а иногда даже усложнит код. Однако при работе с очень большими списками или дорогостоящими шаблонами он может дать ощутимый выигрыш в производительности.

0 0 голоса
Рейтинг статьи
guest
0 комментариев
Старые
Новые Популярные