Skip to content

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

Что такое управление состоянием? ​

Технически каждый экземпляр компонента 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>

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

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

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

state flow diagram

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

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

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

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

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

Простое управление состояниям с помощью 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 не метод компонента.

Хотя здесь мы используем в качестве хранилища один реактивный объект, вы также можете обмениваться реактивным состоянием, созданным с помощью других API реактивности, таких как ref() или computed(), или даже возвращать глобальное состояние из Composable:

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, включая временную шкалу, внутрикомпонентную проверку и отладку с перемещением во времени
  • Механизм позволяющий модулям в приложении обновляться без перезагрузки страницы (Hot Module Replacement)
  • Поддержка рендеринга на стороне сервера

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.

Управление состояниемУже загружено