Реактивное состояние и обновление интерфейса
Оглавление серии «Vue 3: кабинет сотрудников «Команда»»
В предыдущем состоянии кабинет только читает массив. Теперь добавим действие: руководитель нажимает кнопку возле Павла, и подпись «Занят» должна измениться на «Доступен» в той же строке. Новый результат состоит в понимании реактивного источника. Простое изменение произвольной переменной JavaScript ещё не сообщает интерфейсу, что следует обновить текст. Во Vue для такой связи нужен наблюдаемый объект или ref.
Мы продолжим использовать тех же участников, включая их id и version. Сервер пока отсутствует, поэтому действие означает изменение учебной коллекции в памяти. Не называйте его сохранением сотрудника: после повторного создания приложения исходные данные вернутся. Такой словарь пригодится при работе с HTTP, когда локальный экран и долговременная запись могут расходиться.
Ref как источник изменений
ref оборачивает значение и предоставляет реактивную точку доступа. В JavaScript значение находится в свойстве value, а в обычном шаблоне верхнеуровневый ref распаковывается автоматически. Основы реактивности Vue описывают это различие. Не старайтесь применять правило распаковки ко всем возможным контейнерам: вложенные массивы ref и сложные структуры имеют дополнительные условия, которых наш пример не использует.
Полностью заменим src/App.vue. Копия каждого объекта отделяет рабочие значения от исходного набора. Изменение доступности теперь проходит через функцию toggle.
<script setup>
import { ref } from 'vue'
import { seedMembers } from './data/members.js'
const members = ref(seedMembers.map(member => ({ ...member })))
function toggle(id) {
const member = members.value.find(item => item.id === id)
if (member) member.available = !member.available
}
</script>
<template>
<main>
<h1>Команда</h1>
<ul>
<li v-for="member in members" :key="member.id">
{{ member.name }} — {{ member.role }}
<span>{{ member.available ? 'Доступен' : 'Занят' }}</span>
<button type="button" @click="toggle(member.id)">
Изменить доступность {{ member.name }}
</button>
</li>
</ul>
</main>
</template>
При ожидаемом нажатии возле Павла функция получает m-2. find возвращает объект этого участника из реактивной коллекции. Отрицание меняет false на true, и шаблон получает новую подпись. Повторное нажатие возвращает false. Анна и Ирина сохраняют свои значения, поскольку поиск ограничен идентификатором. Передаваемое намерение относится к сотруднику, а не к визуальной позиции его кнопки.
Глубина наблюдения и исходный набор
Для объектов обычный ref создаёт глубокую реактивность. Поэтому присваивание member.available достаточно для нашего изменения: не требуется вручную создавать новый массив после каждого клика. Это поведение отличается от модели, в которой обновление обнаруживается только по замене ссылки. Важно изучить именно договор Vue, а не переносить по привычке правила другого инструмента. При этом иммутабельные замены остаются возможными, если они нужны архитектуре приложения.
Копирование через spread здесь поверхностное, но участники содержат только примитивные поля. Если добавить вложенный объект department, он останется общей ссылкой при таком копировании. Значит утверждение «мы полностью отделили любые данные» было бы неверным. Когда модель усложняется, следует пересмотреть границу копирования: например, явно сформировать нужные вложенные значения. В этом уроке ограниченный состав модели позволяет объяснить результат без скрытого обмена объектами.
seedMembers остаётся справочным набором и не становится источником рабочего состояния. Это удобно при разборе перезагрузки: импорт задаёт начальные значения, ref хранит изменения конкретного экземпляра компонента. Если позже несколько экранов начнут независимо копировать seedMembers, каждый получит собственную команду. Такое поведение может быть полезным для отдельных редакторов, но неудобным для общего кабинета; выделим общий источник лишь после появления маршрутов.
Объявление const members не запрещает менять массив внутри ref. Оно запрещает переназначить переменную members на другой ref. Поэтому members.value может получить другой массив, а поля объектов могут изменяться. Частая путаница состоит в смешении постоянства самой ссылки и неизменности вложенных данных. Для этого примера const помогает сохранить контейнер, а не делает участников доступными исключительно для чтения.
Событие и допустимое действие
Сокращение @click связывает обработчик с событием кнопки. Мы передаём вызов с id, а не меняем поле непосредственно в шаблоне. В первом варианте небольшая запись @click="member.available = !member.available" также могла бы сработать. Однако функция создаёт одно место, где появятся проверки, журнал действия или ограничение по правам. Эта граница имеет конкретную пользу: шаблон сообщает намерение, обработчик определяет изменение.
Обратите внимание на type="button". Сейчас кнопки стоят вне формы, но компонент может позже оказаться внутри неё. Без явного типа обычная кнопка в форме способна инициировать отправку. Такое действие не имеет отношения к переключению доступности. Лучше обозначить смысл уже при создании элемента, чем искать неожиданную перезагрузку после добавления формы. В следующем уроке submit появится только у кнопки, предназначенной для отправки.
Проверка if (member) относится к полезному ошибочному случаю. Переданный неизвестный id не должен приводить к чтению available у undefined. Пока функция просто ничего не делает. Позже API будет различать отсутствие записи и ошибку пользователя, но местный обработчик не получает такой договор. Не добавляйте сообщение «сохранено» при каждом вызове toggle: оно было бы неправдоподобным даже при существующем объекте, потому что долговременной записи ещё нет.
Другая характерная ошибка — обращаться к members.find в JavaScript. Переменная содержит ref, а метод find существует у массива members.value. В шаблоне members работает благодаря распаковке, но это удобство не меняет тип значения внутри script setup. Когда один и тот же идентификатор выглядит рабочим в разметке и ошибочным в функции, сначала уточните, какая именно часть кода читает контейнер, а какая читает его значение.
Момент обновления и ожидаемая проверка
Vue группирует изменения DOM, поэтому не следует считать, что строка страницы обновится буквально в момент присваивания. Модель уже может содержать новое значение, пока DOM ожидает очередной цикл обновления. Если дальнейшее действие потребует измерить элемент после изменения, применяется nextTick; здесь мы не измеряем размеры и не обращаемся к DOM. Добавлять nextTick к каждому обработчику было бы лишним и скрыло бы его настоящую задачу.
Для самостоятельного наблюдения достаточно описать последовательность: исходно Павел занят, первый клик делает его доступным, второй возвращает занятость, пересоздание страницы возвращает исходный массив. При неверном результате проверяйте id, наличие value в JavaScript и тот факт, что шаблон читает members, а не seedMembers. Следующее изменение получит производный список: поиск сможет менять видимые строки без удаления участников из рабочего состояния.
Для самостоятельного воспроизведения результата «Менять занятость сотрудника через ref» выберите lesson-02 в архиве исходников. Статус выполнения этого состояния: not_run.