Управление состоянием
Что это такое?
Технически, каждый экземпляр компонента Vue уже «управляет» своим собственным реактивным состоянием. В качестве примера возьмем простой компонент счётчика:
vue
<script setup>
import { ref } from 'vue'
// состояние
const count = ref(0)
// действия
function increment() {
count.value++
}
</script>
<!-- представление -->
<template>{{ count }}</template>
Это автономный блок, состоящий из следующих частей:
- Состояниe, источник истины, который управляет нашим приложением;
- Представление, декларативное отображение состояния;
- Действия, возможные способы изменения состояния в ответ на пользовательский ввод из представления.
Это простое представление концепции «одностороннего потока данных»:
Однако простота начинает разрушаться, когда у нас есть множество компонентов, имеющих общее состояние:
- Несколько представлений могут зависеть от одного и того же фрагмента состояния.
- Действиям из разных представлений может потребоваться мутировать один и тот же фрагмент состояния.
В первом случае возможным обходным путём является «поднятие» общего состояния до общего компонента-предка, а затем передавать его вниз в виде параметров. Однако это быстро становится утомительным в деревьях компонентов с глубокой иерархией, что приводит к другой проблеме, известной как сквозная передача параметров.
Во втором случае мы часто прибегаем к таким решениям, как поиск прямых родительских и дочерних экземпляров с помощью шаблонных ссылок или попытка мутировать и синхронизировать несколько копий состояния с помощью испускаемых событий. Оба этих паттерна хрупки и быстро приводят к появлению не поддерживаемого кода.
Более простым и понятным решением является извлечение общего состояния из компонентов и управление им в глобальном синглтоне. Таким образом, наше дерево компонентов превращается в большое «представление», и любой компонент может получить доступ к состоянию или вызвать действия, независимо от того, где он находится в дереве!
Простое управление состоянием с помощью Reactivity API
Если у вас есть часть состояния, которая должна быть общей для нескольких экземпляров, вы можете использовать reactive()
для создания реактивного объекта, а затем импортировать его в несколько компонентов:
js
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0
})
vue
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>
<template>Из A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>
<template>Из B: {{ store.count }}</template>
Теперь при каждом изменении объекта store
и <ComponentA>
, и <ComponentB>
будут автоматически обновлять свои представления - у нас теперь есть единый источник истины.
Однако это также означает, что любой компонент, импортирующий store
, может изменять его по своему усмотрению:
template
<template>
<button @click="store.count++">
Из B: {{ store.count }}
</button>
</template>
Хотя в простых случаях это работает, глобальное состояние, которое может произвольно изменяться любым компонентом, в долгосрочной перспективе будет не очень удобным для обслуживания. Чтобы логика изменения состояния была централизованной, как и само состояние, рекомендуется определять методы в хранилище с именами, выражающими намерение действий:
js
// store.js
import { reactive } from 'vue'
export const store = reactive({
count: 0,
increment() {
this.count++
}
})
template
<template>
<button @click="store.increment()">
Из B: {{ store.count }}
</button>
</template>
Примечание
Обратите внимание, что в обработчике клика используется store.increment()
со скобками - это необходимо для вызова метода с правильным контекстом this
, поскольку это не метод компонента.
Хотя здесь мы используем один реактивный объект в качестве хранилища, вы также можете делиться реактивным состоянием, созданным с помощью других Reactivity API, таких как ref()
или computed()
, или даже возвращать глобальное состояние из A композабла:
js
import { ref } from 'vue'
// глобальное состояние, созданное в области видимости модуля
const globalCount = ref(1)
export function useCount() {
// локальное состояние, создаваемое для каждого компонента
const localCount = ref(1)
return {
globalCount,
localCount
}
}
Тот факт, что система реактивности Vue отделена от компонентной модели, делает её чрезвычайно гибкой.
Соображения по SSR
Если вы создаете приложение, использующее рендеринг на стороне сервера (SSR), описанный выше паттерн может привести к проблемам, поскольку хранилище является синглтоном, разделяемым между несколькими запросами. Это обсуждается подробнее в руководстве по SSR.
Pinia
Хотя нашего решения для управления состоянием вручную будет достаточно в простых сценариях, есть ещё много вещей, которые следует учитывать в крупномасштабных продакшен-приложениях:
- Более сильные соглашения для совместной работы в команде
- Интеграция с инструментами Vue DevTools, включая временную шкалу, внутрикомпонентную проверку и отладку во времени
- Замена горячего модуля
- Поддержка рендеринга на стороне сервера
Pinia — это библиотека управления состояниями, которая реализует всё вышеперечисленное. Она поддерживается основной командой Vue и работает как с Vue 2, так и с Vue 3.
Бывалые пользователи могут быть знакомы с Vuex, предыдущей официальной библиотекой управления состояниями для Vue. Поскольку Pinia выполняет ту же роль в экосистеме, Vuex сейчас находится в режиме обслуживания. Она по-прежнему работает, но больше не будет получать новые функции. Для новых приложений рекомендуется использовать Pinia.
Pinia начиналась как исследование того, как может выглядеть следующая итерация Vuex, вобрав в себя множество идей из обсуждений основной команды Vuex 5. В конце концов, мы поняли, что Pinia уже реализует большую часть того, что мы хотели во Vuex 5, и решили сделать её новой рекомендацией.
По сравнению с Vuex, Pinia предоставляет более простой API с меньшим количеством церемоний, предлагает API в стиле Composition API, и, что самое важное, имеет надежную поддержку вывода типов при использовании TypeScript.