Перейти к содержанию

Управление состоянием ​

Что это такое? ​

Технически, каждый экземпляр компонента Vue уже «управляет» своим собственным реактивным состоянием. В качестве примера возьмем простой компонент счётчика:

vue
<script setup>
import { ref } from 'vue'

// состояние
const count = ref(0)

// действия
function increment() {
  count.value++
}
</script>

<!-- представление -->
<template>{{ count }}</template>
vue
<script>
export default {
  // состояние
  data() {
    return {
      count: 0
    }
  },
  // действия
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<!-- представление -->
<template>{{ count }}</template>

Это автономный блок, состоящий из следующих частей:

  • Состояниe, источник истины, который управляет нашим приложением;
  • Представление, декларативное отображение состояния;
  • Действия, возможные способы изменения состояния в ответ на пользовательский ввод из представления.

Это простое представление концепции «одностороннего потока данных»:

диаграмма потока состояний

Однако простота начинает разрушаться, когда у нас есть множество компонентов, имеющих общее состояние:

  1. Несколько представлений могут зависеть от одного и того же фрагмента состояния.
  2. Действиям из разных представлений может потребоваться мутировать один и тот же фрагмент состояния.

В первом случае возможным обходным путём является «поднятие» общего состояния до общего компонента-предка, а затем передавать его вниз в виде параметров. Однако это быстро становится утомительным в деревьях компонентов с глубокой иерархией, что приводит к другой проблеме, известной как сквозная передача параметров.

Во втором случае мы часто прибегаем к таким решениям, как поиск прямых родительских и дочерних экземпляров с помощью шаблонных ссылок или попытка мутировать и синхронизировать несколько копий состояния с помощью испускаемых событий. Оба этих паттерна хрупки и быстро приводят к появлению не поддерживаемого кода.

Более простым и понятным решением является извлечение общего состояния из компонентов и управление им в глобальном синглтоне. Таким образом, наше дерево компонентов превращается в большое «представление», и любой компонент может получить доступ к состоянию или вызвать действия, независимо от того, где он находится в дереве!

Простое управление состоянием с помощью Reactivity API ​

В Options API реактивные данные объявляются с помощью опции data(). Внутренне объект, возвращаемый data(), становится реактивным с помощью функции reactive(), которая также доступна в виде публичного API.

Если у вас есть часть состояния, которая должна быть общей для нескольких экземпляров, вы можете использовать reactive() для создания реактивного объекта, а затем импортировать его в несколько компонентов:

store.js
js
import { reactive } from 'vue'

export const store = reactive({
  count: 0
})
ComponentA.vue
vue
<script setup>
import { store } from './store.js'
</script>

<template>Из A: {{ store.count }}</template>
ComponentB.vue
vue
<script setup>
import { store } from './store.js'
</script>

<template>Из B: {{ store.count }}</template>
ComponentA.vue
vue
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>Из A: {{ store.count }}</template>
ComponentB.vue
vue
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>Из B: {{ store.count }}</template>

Теперь при каждом изменении объекта store и <ComponentA>, и <ComponentB> будут автоматически обновлять свои представления - у нас теперь есть единый источник истины.

Однако это также означает, что любой компонент, импортирующий store, может изменять его по своему усмотрению:

template
<template>
  <button @click="store.count++">
    Из B: {{ store.count }}
  </button>
</template>

Хотя в простых случаях это работает, глобальное состояние, которое может произвольно изменяться любым компонентом, в долгосрочной перспективе будет не очень удобным для обслуживания. Чтобы логика изменения состояния была централизованной, как и само состояние, рекомендуется определять методы в хранилище с именами, выражающими намерение действий:

store.js
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.

Управление состоянием