Философия Vue 3, реактивность, шаблонный синтаксис, директивы, v-model, вычисляемые свойства и жизненный цикл компонента
Vue 3 — прогрессивный JavaScript-фреймворк для построения пользовательских интерфейсов. Его философия: минимальное ядро с мощной системой реактивности, компонентной архитектурой и декларативным шаблонным синтаксисом.
Vue 3 — это реактивный UI-фреймворк, построенный вокруг двух ключевых идей: реактивности и компонентности. Реактивность означает, что когда данные меняются, интерфейс автоматически обновляется — вам не нужно вручную манипулировать DOM. Компонентность означает, что интерфейс строится из изолированных переиспользуемых блоков — компонентов.
Vue 3 следует принципу прогрессивности: вы можете добавить его в существующую страницу через тег <script> или построить полноценное SPA с помощью Vite. Фреймворк не навязывает архитектуру — начните с малого, а когда нужно, масштабируйтесь.
Три главных принципа Vue:
.vueДля быстрого прототипирования или добавления Vue в существующий HTML достаточно подключить его через CDN.
Самый простой способ попробовать Vue — подключить его прямо в HTML-файл через CDN:
<!DOCTYPE html>
<html>
<head>
<title>Мой первый Vue</title>
</head>
<body>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
setup() {
const message = ref('Привет, Vue!')
return { message }
}
}).mount('#app')
</script>
</body>
</html>Этот подход подходит для обучения, но для реальных проектов нужен полноценный инструментарий. Для создания полноценного Vue-проекта используйте официальный скаффолдинг через npm:
npm create vue@latest my-project
cd my-project
npm install
npm run devКоманда npm create vue@latest запустит интерактивный мастер, где можно выбрать TypeScript, Vue Router, Pinia, ESLint и другие опции. Под капотом используется Vite — молниеносный сборщик на базе ES-модулей.
После установки структура проекта выглядит так:
my-project/
├── src/
│ ├── assets/ # Статические файлы (CSS, изображения)
│ ├── components/ # Переиспользуемые компоненты
│ ├── App.vue # Корневой компонент
│ └── main.js # Точка входа
├── public/ # Файлы без обработки
├── index.html # HTML-шаблон
├── vite.config.js # Конфигурация Vite
└── package.json
Каждый компонент Vue хранится в файле .vue. Это так называемый Single File Component — концепция, при которой шаблон, скрипт и стили живут вместе.
Файл .vue состоит из трёх блоков:
<template>
<!-- HTML-разметка компонента -->
<div class="counter">
<p>Счётчик: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
// Логика компонента
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
<style scoped>
/* Стили — scoped означает что они применяются только к этому компоненту */
.counter {
padding: 16px;
}
</style>Директива scoped в <style> важна: она добавляет уникальный атрибут ко всем элементам компонента и гарантирует, что стили не вытекут в другие компоненты.
В Vue 3 есть два способа писать логику компонента. Options API — классический стиль, где логика разделена по «опциям» объекта. Composition API — новый стиль Vue 3, где логика организована по функциям.
Тот же компонент-счётчик на Options API выглядит так:
<script>
export default {
data() {
return { count: 0 }
},
computed: {
doubled() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Компонент создан, count =', this.count)
}
}
</script>А на Composition API с <script setup> тот же результат выглядит компактнее и логика группируется по смыслу, а не по типу опции:
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('Компонент создан, count =', count.value)
})
</script>Options API остаётся полностью поддерживаемым и хорош для небольших компонентов. Composition API — предпочтительный выбор для сложной логики и максимальной переиспользуемости кода. В следующей теме мы подробно разберём Composition API.
Шаблоны Vue — это расширенный HTML. Vue компилирует шаблоны в оптимизированный JavaScript-код, который обновляет DOM только там, где нужно.
Самый базовый способ вывести данные — интерполяция через {{ }}. Внутри скобок можно использовать любое JavaScript-выражение:
<template>
<div>
<p>{{ message }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ ok ? 'Да' : 'Нет' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
</div>
</template>Важно: {{ }} всегда выводит текст, HTML-теги будут экранированы. Это защита от XSS-атак.
:Для привязки данных к HTML-атрибутам используется директива v-bind. Она связывает атрибут с реактивным значением:
<template>
<!-- Полный синтаксис -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- Сокращение через двоеточие (рекомендуется) -->
<img :src="imageUrl" :alt="imageAlt">
<!-- Привязка динамического имени атрибута -->
<button :[dynamicAttr]="value">Кнопка</button>
<!-- Привязка объекта атрибутов целиком -->
<div v-bind="{ id: 'hero', class: 'active' }">Секция</div>
</template>
<script setup>
const imageUrl = 'https://example.com/logo.png'
const imageAlt = 'Логотип'
const dynamicAttr = 'disabled'
const value = true
</script>@Для обработки событий DOM используется v-on. Этот синтаксис позволяет вызывать методы или писать инлайн-выражения:
<template>
<!-- Полный синтаксис -->
<button v-on:click="handleClick">Нажми</button>
<!-- Сокращение @ (рекомендуется) -->
<button @click="handleClick">Нажми</button>
<!-- Инлайн-выражение -->
<button @click="count++">+1</button>
<!-- С модификаторами -->
<form @submit.prevent="onSubmit">
<input @keydown.enter="onEnter">
</form>
</template>Модификаторы событий очень удобны: .prevent вызывает event.preventDefault(), .stop — event.stopPropagation(), .once — обработчик сработает только раз.
Директивы — это специальные атрибуты с префиксом v-, которые добавляют реактивное поведение DOM-элементам.
Vue рендерит или скрывает элементы в зависимости от условия. При v-if элемент полностью создаётся или уничтожается в DOM:
<template>
<div>
<p v-if="userType === 'admin'">Панель администратора</p>
<p v-else-if="userType === 'moderator'">Панель модератора</p>
<p v-else>Обычный пользователь</p>
</div>
</template>
<script setup>
const userType = 'admin'
</script>v-show в отличие от v-if не удаляет элемент из DOM, а только устанавливает display: none. Это выгоднее когда элемент часто переключается:
<template>
<!-- Элемент присутствует в DOM, но скрыт если isVisible = false -->
<div v-show="isVisible">Этот блок может скрываться</div>
</template>Правило выбора: используйте v-if если условие редко меняется (дорогой первоначальный рендер оправдан), v-show если состояние часто переключается (например, дропдаун-меню).
Директива v-for позволяет рендерить список элементов на основе массива или объекта. Атрибут :key обязателен — он помогает Vue отслеживать изменения списка:
<template>
<ul>
<!-- Перебор массива -->
<li v-for="item in items" :key="item.id">
{{ item.name }} — {{ item.price }} руб.
</li>
</ul>
<!-- С индексом -->
<ol>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ol>
<!-- Перебор объекта -->
<div v-for="(value, key) in userProfile" :key="key">
{{ key }}: {{ value }}
</div>
</template>
<script setup>
const items = [
{ id: 1, name: 'Яблоко', price: 50 },
{ id: 2, name: 'Банан', price: 30 },
{ id: 3, name: 'Апельсин', price: 70 }
]
const userProfile = {
name: 'Иван',
email: 'ivan@example.com',
role: 'developer'
}
</script>Почему :key обязателен? Без ключей Vue не понимает, какой элемент соответствует какому DOM-узлу при перестановке. Это приводит к багам — неправильные DOM-узлы обновляются, теряется состояние полей ввода. Всегда используйте уникальный идентификатор в качестве ключа (не индекс массива — при сортировке это сломается).
v-model — синтаксический сахар для одновременной привязки значения и обработки события ввода. Используется с <input>, <textarea>, <select>:
<template>
<div>
<input v-model="name" placeholder="Ваше имя">
<p>Привет, {{ name }}!</p>
<!-- Числовая привязка — автоматически преобразует в число -->
<input v-model.number="age" type="number">
<!-- .trim удаляет пробелы по краям -->
<input v-model.trim="email" type="email">
<!-- .lazy обновляет только при blur (потере фокуса) -->
<input v-model.lazy="description" type="text">
<!-- Чекбокс -->
<input v-model="isAgree" type="checkbox">
<!-- Выпадающий список -->
<select v-model="selectedCity">
<option value="">Выберите город</option>
<option value="moscow">Москва</option>
<option value="spb">Санкт-Петербург</option>
</select>
</div>
</template>
<script setup>
import { ref } from 'vue'
const name = ref('')
const age = ref(0)
const email = ref('')
const description = ref('')
const isAgree = ref(false)
const selectedCity = ref('')
</script>Под капотом v-model="name" — это v-bind:value="name" @input="name = $event.target.value". Vue разворачивает это в привязку и обработчик события.
Computed-свойства позволяют вычислять значения на основе реактивных данных. В отличие от методов, computed кэшируется — значение пересчитывается только когда зависимые данные изменились:
<template>
<div>
<p>Полное имя: {{ fullName }}</p>
<p>Отфильтрованных товаров: {{ filteredProducts.length }}</p>
</div>
</template>
<script>
export default {
data() {
return {
firstName: 'Иван',
lastName: 'Петров',
products: [
{ name: 'Ноутбук', inStock: true },
{ name: 'Мышь', inStock: false },
{ name: 'Клавиатура', inStock: true }
]
}
},
computed: {
// Геттер — только для чтения
fullName() {
return `${this.firstName} ${this.lastName}`
},
// Вычисление на основе массива
filteredProducts() {
return this.products.filter(p => p.inStock)
}
}
}
</script>Computed с геттером и сеттером используется когда нужно двусторонняя привязка:
<script>
export default {
data() {
return {
firstName: 'Иван',
lastName: 'Петров'
}
},
computed: {
fullName: {
get() {
return `${this.firstName} ${this.lastName}`
},
set(value) {
const parts = value.split(' ')
this.firstName = parts[0]
this.lastName = parts[1] || ''
}
}
}
}
</script>Методы — функции компонента, доступные из шаблона. Они не кэшируются и вызываются заново каждый раз. Используйте методы для обработки событий и выполнения действий:
<template>
<div>
<button @click="addToCart(product)">В корзину</button>
<button @click="removeFromCart(product.id)">Удалить</button>
<p>Итого: {{ formatPrice(totalPrice) }}</p>
</div>
</template>
<script>
export default {
data() {
return {
cart: [],
product: { id: 1, name: 'Книга', price: 500 }
}
},
computed: {
totalPrice() {
return this.cart.reduce((sum, item) => sum + item.price, 0)
}
},
methods: {
addToCart(item) {
this.cart.push({ ...item })
},
removeFromCart(id) {
this.cart = this.cart.filter(item => item.id !== id)
},
formatPrice(price) {
return `${price.toLocaleString('ru-RU')} руб.`
}
}
}
</script>Вопрос «метод или computed?» решается просто: если нужен результат вычисления без аргументов — computed (кэшируется), если нужна функция с аргументами или действие (изменение данных, запрос к API) — метод.
Каждый компонент Vue проходит через предсказуемые этапы создания, обновления и уничтожения. Хуки жизненного цикла позволяют запускать код на определённых этапах:
<script>
export default {
data() {
return {
posts: []
}
},
// Компонент создан (данные инициализированы, но DOM ещё нет)
created() {
console.log('created: DOM недоступен, но данные уже есть')
// Хорошее место для запросов к API
this.loadPosts()
},
// Компонент смонтирован в DOM
mounted() {
console.log('mounted: DOM доступен')
// Хорошее место для работы с DOM, подключения сторонних библиотек
this.$el.querySelector('input')?.focus()
},
// Данные обновились, DOM обновляется
updated() {
console.log('updated: компонент перерисован')
},
// Компонент удалён из DOM
unmounted() {
console.log('unmounted: очищаем ресурсы')
// Отписываемся от событий, таймеров, WebSocket
clearInterval(this.timer)
},
methods: {
async loadPosts() {
const response = await fetch('/api/posts')
this.posts = await response.json()
}
}
}
</script>Также существуют хуки beforeCreate, beforeMount, beforeUpdate, beforeUnmount — они вызываются до соответствующего этапа. На практике чаще всего используются mounted (загрузка данных, DOM-манипуляции) и unmounted (очистка ресурсов).
Давайте объединим всё изученное в реальный компонент. Это карточка товара с добавлением в корзину:
<template>
<div class="product-card" :class="{ 'out-of-stock': !product.inStock }">
<img :src="product.image" :alt="product.name">
<div class="product-info">
<h3>{{ product.name }}</h3>
<p class="price">{{ formattedPrice }}</p>
<span v-if="product.inStock" class="badge badge-success">В наличии</span>
<span v-else class="badge badge-error">Нет в наличии</span>
<div class="quantity-control" v-show="product.inStock">
<button @click="quantity--" :disabled="quantity <= 1">-</button>
<span>{{ quantity }}</span>
<button @click="quantity++">+</button>
</div>
<button
@click="addToCart"
:disabled="!product.inStock"
class="btn-primary"
>
{{ product.inStock ? 'В корзину' : 'Недоступно' }}
</button>
</div>
</div>
</template>
<script>
export default {
props: {
product: {
type: Object,
required: true
}
},
emits: ['add-to-cart'],
data() {
return {
quantity: 1
}
},
computed: {
formattedPrice() {
return `${(this.product.price * this.quantity).toLocaleString('ru-RU')} руб.`
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', {
product: this.product,
quantity: this.quantity
})
this.quantity = 1
}
}
}
</script>В этом компоненте мы использовали: привязку классов (:class), атрибутов (:src, :disabled), обработчики событий (@click), условный рендеринг (v-if/v-else, v-show), computed и props с emits (их подробно разберём в теме про компоненты).
Вы изучили фундамент Vue 3: реактивность через Options API, шаблонный синтаксис с интерполяцией и v-bind/v-on, ключевые директивы v-if, v-show, v-for, v-model, вычисляемые свойства и хуки жизненного цикла. Эти знания — основа для всего остального курса. В следующей теме переходим к Composition API — современному способу организации логики в Vue 3.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.