**Pinia** — официальное хранилище состояния для Vue 3 (заменила Vuex). Она предоставляет централизованное хранилище для данных, которые нужно разделять между несколькими компонентами.
Ключевые преимущества перед Vuex:
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Синтаксис Composition API (рекомендуемый)
export const useCounterStore = defineStore('counter', () => {
// state — реактивные переменные
const count = ref(0)
const step = ref(1)
// getters — вычисляемые свойства
const doubled = computed(() => count.value * 2)
const canDecrement = computed(() => count.value > 0)
// actions — методы для изменения state
function increment() {
count.value += step.value
}
function decrement() {
if (canDecrement.value) count.value -= step.value
}
async function fetchCount() {
const res = await fetch('/api/count')
count.value = await res.json()
}
return { count, step, doubled, canDecrement, increment, decrement, fetchCount }
})<template>
<div>
<p>Счётчик: {{ counter.count }}</p>
<p>Удвоенный: {{ counter.doubled }}</p>
<button @click="counter.increment()">+{{ counter.step }}</button>
<button @click="counter.decrement()" :disabled="!counter.canDecrement">
-{{ counter.step }}
</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// counter.count, counter.doubled — реактивны
// counter.increment() — вызов action
</script>// Альтернативный синтаксис (похож на Vuex)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
step: 1,
}),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() {
this.count += this.step
},
async fetchCount() {
this.count = await fetch('/api/count').then(r => r.json())
}
}
})| Возможность | Pinia | Vuex 4 |
|---|---|---|
| Мутации | Нет (actions напрямую) | Обязательные |
| TypeScript | Отличная | Посредственная |
| DevTools | Да | Да |
| Модули | Каждый store — модуль | Вложенные модули |
| Размер | ~1 KB | ~10 KB |
| Composition API | Нативно | Через хелперы |
// Персистентность через плагин или вручную
import { watch } from 'vue'
const store = useUserStore()
// Загрузить при старте
const saved = localStorage.getItem('user-store')
if (saved) Object.assign(store, JSON.parse(saved))
// Сохранять при изменениях
watch(
() => store.$state,
(state) => localStorage.setItem('user-store', JSON.stringify(state)),
{ deep: true }
)Pinia автоматически интегрируется с Vue DevTools: можно видеть все сторы, их state, историю actions и делать time-travel debugging.
Мини-стор через замыкание — аналог того, как Pinia организует state, getters и actions
// Реализация паттерна Pinia через замыкания в чистом JS.
// Singleton-сторы: один экземпляр на id.
const _stores = new Map()
function defineStore(id, setup) {
// Возвращаем фабричную функцию (useXxxStore)
return function useStore() {
// Singleton: если стор уже создан — возвращаем его
if (_stores.has(id)) {
return _stores.get(id)
}
// Создаём стор один раз
const rawResult = setup()
// Разделяем на state, getters и actions
const state = {}
const getters = {}
const actions = {}
for (const [key, value] of Object.entries(rawResult)) {
if (typeof value === 'function') {
// Функции — это getters или actions
// Различаем: getters не принимают аргументов (условно)
actions[key] = value
} else {
state[key] = value
}
}
// Создаём Proxy для прозрачного доступа
const store = new Proxy(
{ ...state },
{
get(target, key) {
// Приоритет: actions > state
if (key in actions) return actions[key].bind(store)
if (key in target) return target[key]
if (key === '$state') return { ...target }
if (key === '$reset') {
return () => {
const fresh = setup()
for (const k of Object.keys(state)) {
if (!(fresh[k] instanceof Function)) {
target[k] = fresh[k]
}
}
console.log(`[${id}] $reset`)
}
}
},
set(target, key, value) {
const old = target[key]
target[key] = value
console.log(`[${id}] ${key}: ${JSON.stringify(old)} -> ${JSON.stringify(value)}`)
return true
}
}
)
_stores.set(id, store)
return store
}
}
// --- Создаём стор корзины покупок ---
const useCartStore = defineStore('cart', () => {
const items = []
const discount = 0
function addItem(item) {
items.push(item)
console.log(`[cart] Добавлен: ${item.name}`)
}
function removeItem(id) {
const idx = items.findIndex(i => i.id === id)
if (idx !== -1) {
console.log(`[cart] Удалён: ${items[idx].name}`)
items.splice(idx, 1)
}
}
function getTotal() {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0)
return sum * (1 - discount / 100)
}
return { items, discount, addItem, removeItem, getTotal }
})
// --- Демонстрация singleton ---
console.log('=== Singleton: один экземпляр ===')
const cart1 = useCartStore()
const cart2 = useCartStore()
console.log('cart1 === cart2:', cart1 === cart2) // true
console.log('\n=== Работа со стором ===')
cart1.addItem({ id: 1, name: 'Vue 3 курс', price: 1000, qty: 1 })
cart1.addItem({ id: 2, name: 'Pinia книга', price: 500, qty: 2 })
cart2.discount = 10 // cart1 и cart2 — один объект
console.log('Итого:', cart1.getTotal()) // (1000 + 1000) * 0.9 = 1800
console.log('Items count:', cart1.items.length)**Pinia** — официальное хранилище состояния для Vue 3 (заменила Vuex). Она предоставляет централизованное хранилище для данных, которые нужно разделять между несколькими компонентами.
Ключевые преимущества перед Vuex:
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Синтаксис Composition API (рекомендуемый)
export const useCounterStore = defineStore('counter', () => {
// state — реактивные переменные
const count = ref(0)
const step = ref(1)
// getters — вычисляемые свойства
const doubled = computed(() => count.value * 2)
const canDecrement = computed(() => count.value > 0)
// actions — методы для изменения state
function increment() {
count.value += step.value
}
function decrement() {
if (canDecrement.value) count.value -= step.value
}
async function fetchCount() {
const res = await fetch('/api/count')
count.value = await res.json()
}
return { count, step, doubled, canDecrement, increment, decrement, fetchCount }
})<template>
<div>
<p>Счётчик: {{ counter.count }}</p>
<p>Удвоенный: {{ counter.doubled }}</p>
<button @click="counter.increment()">+{{ counter.step }}</button>
<button @click="counter.decrement()" :disabled="!counter.canDecrement">
-{{ counter.step }}
</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// counter.count, counter.doubled — реактивны
// counter.increment() — вызов action
</script>// Альтернативный синтаксис (похож на Vuex)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
step: 1,
}),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() {
this.count += this.step
},
async fetchCount() {
this.count = await fetch('/api/count').then(r => r.json())
}
}
})| Возможность | Pinia | Vuex 4 |
|---|---|---|
| Мутации | Нет (actions напрямую) | Обязательные |
| TypeScript | Отличная | Посредственная |
| DevTools | Да | Да |
| Модули | Каждый store — модуль | Вложенные модули |
| Размер | ~1 KB | ~10 KB |
| Composition API | Нативно | Через хелперы |
// Персистентность через плагин или вручную
import { watch } from 'vue'
const store = useUserStore()
// Загрузить при старте
const saved = localStorage.getItem('user-store')
if (saved) Object.assign(store, JSON.parse(saved))
// Сохранять при изменениях
watch(
() => store.$state,
(state) => localStorage.setItem('user-store', JSON.stringify(state)),
{ deep: true }
)Pinia автоматически интегрируется с Vue DevTools: можно видеть все сторы, их state, историю actions и делать time-travel debugging.
Мини-стор через замыкание — аналог того, как Pinia организует state, getters и actions
// Реализация паттерна Pinia через замыкания в чистом JS.
// Singleton-сторы: один экземпляр на id.
const _stores = new Map()
function defineStore(id, setup) {
// Возвращаем фабричную функцию (useXxxStore)
return function useStore() {
// Singleton: если стор уже создан — возвращаем его
if (_stores.has(id)) {
return _stores.get(id)
}
// Создаём стор один раз
const rawResult = setup()
// Разделяем на state, getters и actions
const state = {}
const getters = {}
const actions = {}
for (const [key, value] of Object.entries(rawResult)) {
if (typeof value === 'function') {
// Функции — это getters или actions
// Различаем: getters не принимают аргументов (условно)
actions[key] = value
} else {
state[key] = value
}
}
// Создаём Proxy для прозрачного доступа
const store = new Proxy(
{ ...state },
{
get(target, key) {
// Приоритет: actions > state
if (key in actions) return actions[key].bind(store)
if (key in target) return target[key]
if (key === '$state') return { ...target }
if (key === '$reset') {
return () => {
const fresh = setup()
for (const k of Object.keys(state)) {
if (!(fresh[k] instanceof Function)) {
target[k] = fresh[k]
}
}
console.log(`[${id}] $reset`)
}
}
},
set(target, key, value) {
const old = target[key]
target[key] = value
console.log(`[${id}] ${key}: ${JSON.stringify(old)} -> ${JSON.stringify(value)}`)
return true
}
}
)
_stores.set(id, store)
return store
}
}
// --- Создаём стор корзины покупок ---
const useCartStore = defineStore('cart', () => {
const items = []
const discount = 0
function addItem(item) {
items.push(item)
console.log(`[cart] Добавлен: ${item.name}`)
}
function removeItem(id) {
const idx = items.findIndex(i => i.id === id)
if (idx !== -1) {
console.log(`[cart] Удалён: ${items[idx].name}`)
items.splice(idx, 1)
}
}
function getTotal() {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0)
return sum * (1 - discount / 100)
}
return { items, discount, addItem, removeItem, getTotal }
})
// --- Демонстрация singleton ---
console.log('=== Singleton: один экземпляр ===')
const cart1 = useCartStore()
const cart2 = useCartStore()
console.log('cart1 === cart2:', cart1 === cart2) // true
console.log('\n=== Работа со стором ===')
cart1.addItem({ id: 1, name: 'Vue 3 курс', price: 1000, qty: 1 })
cart1.addItem({ id: 2, name: 'Pinia книга', price: 500, qty: 2 })
cart2.discount = 10 // cart1 и cart2 — один объект
console.log('Итого:', cart1.getTotal()) // (1000 + 1000) * 0.9 = 1800
console.log('Items count:', cart1.items.length)Реализуй функцию `defineStore(id, setup)` — аналог Pinia. `setup` — функция, возвращающая объект, где обычные значения — это state, а функции — actions. `defineStore` должна возвращать фабрику (функцию `useStore`), которая при каждом вызове возвращает один и тот же объект (singleton на основе `id`). Стор должен предоставлять метод `$reset()` — сбрасывает state к начальным значениям.
В registry храни результат setup() привязанный к id. Разбери возвращаемый объект: typeof value === 'function' — action, иначе — state. Для $reset() снова вызови setup() и скопируй только нефункциональные свойства обратно в объект стора через Object.assign или цикл.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке