Руководство по двусторонней привязке в Vue

Привязка данных - это фундаментальная концепция в Vue.js, которая синхронизирует данные между представлением и базовой логикой (моделью) приложения.

По умолчанию Vue.js используется односторонняя привязка данных, при которой данные передаются в одном направлении - от родительского компонента к дочернему компоненту или от объекта JavaScript к шаблону. Данные не передаются в обратном направлении:

<template>
   <div>
       <p>{{message}}</p>
   </div>
</template>

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

   const message = ref('Hello, Vue!');
</script>

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

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

Именно здесь вступает в действие двусторонняя привязка данных.

Что такое двусторонняя привязка?

Двусторонняя привязка данных восполняет недостаток устаревшей привязки данных в Vue.js, позволяя передавать данные в обоих направлениях - от модели к представлению и наоборот.

Представьте себе двустороннюю привязку данных как синхронизацию в реальном времени между объектом данных и DOM, аналогичную двустороннему зеркальному отображению. Когда свойство данных обновляется, оно немедленно отражается в любых связанных элементах DOM с помощью директивы, подобной v-model.

Аналогично, когда пользователь взаимодействует с DOM - например, вводит данные в поле ввода, привязанное к v-модели, - данные в экземпляре Vue обновляются автоматически, создавая цикл обратной связи в реальном времени (двусторонний) между данными и DOM.

Рассмотрим предыдущий пример, но теперь с формой:

<template>
   <div>
        <form action="/">
           <label>Input box</label>
           <input v-model="message" />
        </form>
        <div>
           <p>Message:</p>
           <span>{{message}}</span>
        </div>
   </div>
</template>

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

   const message = ref('Hello, Vue!');
</script>

Используя директиву v-model, мы можем не только динамически отображать значение переменной message в шаблоне, но также редактировать и модифицировать его с помощью поля ввода в элементе Form.

An interactive example showing two-way data binding in Vue.js where an input field and a message update simultaneously.

Эта функциональность присуща не только элементу ввода; другие компоненты формы, такие как флажки и переключатели, работают аналогичным образом.

Учитывая все сказанное об односторонней и двусторонней привязке данных, давайте рассмотрим некоторые практические примеры использования директивы v-model.

Создание пользовательских компонентов v-образной модели

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

Директива v-model упрощает создание пользовательских компонентов, поддерживающих двустороннюю привязку данных. Она позволяет пользовательскому компоненту получать значение prop от своего родительского компонента, которое может быть привязано к элементу ввода в шаблоне. Затем компонент может генерировать событие, сигнализирующее об изменениях родительскому компоненту.

Diagram illustrating two-way data binding in Vue.js with a custom component and a parent component using v-model and input events.

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

//components/Form.vue

<template>
 <form action="/">
   <label>Input box</label>
   <input v-model="message"/>
 </form>
</template>

С учетом этих изменений директива v-model для элемента input больше не имеет доступа к переменной message, что делает ее неэффективной в новом компоненте Form.

Чтобы решить эту проблему, нам нужно удалить атрибут v-model="message" из элемента input и вместо этого добавить его в объявление компонента <Form/> в родительском компоненте следующим образом:

//App.vue

<script setup>
import {ref} from "vue";
import Form from "@/components/Form.vue";

const message = ref('Hello, Vue!');

</script>

<template>
 <div class="greetings">
   <Form v-model="message" />
   ...
 </div>
</template>

На этот раз переменная message будет привязана ко всему компоненту, а не только к элементу ввода.

Теперь мы можем получить доступ к данным внутри компонента с помощью defineProps и передать их в поле ввода с помощью директивы :value следующим образом:

//components/Form.vue

<template>
 <form action="/">
   <label>Input box</label>
   <input :value="modelValue" />
 </form>
</template>

<script setup>
 defineProps(['modelValue']);
</script>

modelValue - это специальное имя prop, используемое для передачи значения связанной переменной дочернему компоненту. До сих пор мы обрабатывали только нисходящий поток данных между этими компонентами. Если вы проверите браузер, в поле ввода теперь должно отображаться значение переменной сообщения, но оно не может обновить данные:

A Vue.js example demonstrating real-time updates between an input field and a message using the v-model directive.

Чтобы обработать восходящий поток данных и обновить данные из компонента Form, нам нужно зафиксировать значение поля ввода и передать его с помощью события update:modelValue:

//component/Form.vue

<script setup>
 defineProps(['modelValue']);

 const emit = defineEmits(['update:modelValue']);

 const updateModel = (e)=>{
   const value = e.target.value;
   emit('update:modelValue', value);
 };

</script>

<template>
 <form action="/">
   <label>Input box</label>
   <input @input="updateModel" :value="modelValue" />
 </form>
</template>

Update:modelValue - это встроенное событие, которое обрабатывает двустороннюю привязку данных в пользовательских компонентах. Когда компонент генерирует это событие с новым значением, оно сигнализирует родительскому компоненту об обновлении привязанной переменной (в данном случае message) новым значением.

Из-за этого нам не нужно вносить дополнительные изменения в родительский компонент - он автоматически обновит переменную message, когда компонент Form выдаст новое значение.

Теперь оба компонента связаны, но наш код несколько многословен. Если бы мы реализовали это в более широком масштабе, то с кодом было бы быстро сложнее справляться.

Далее мы рассмотрим, как мы можем упростить двустороннюю привязку данных, используя новую служебную функцию defineModel.

Упрощение с помощью defineModel

В Vue 3 Composition API появилось несколько инновационных функций и были внесены существенные улучшения в существующие. Одним из таких улучшений является упрощение двусторонней привязки данных с помощью макроса defineModel, представленного в Vue 3.4.

Макрос defineModel автоматизирует процесс настройки параметра modelValue prop и события update:modelValue, создавая реактивную ссылку на параметр и автоматически обновляя ее при изменении значения поля ввода.

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

Мы можем использовать defineModel для упрощения нашего кода следующим образом:

//components/Form.vue

<script setup>
  const inputValue = defineModel();
</script>

<template>
 <form action="/">
   <label>Input box</label>
   <input v-model="inputValue" />
 </form>
</template>

В данном случае defineModel заботится как о синхронизации данных, так и о передаче событий. Ниже приводится разбивка необходимых шагов:

    Объявление параметра modelValue (входное значение) Строка const inputValue = defineModel(); объявляет параметр modelValue в компоненте. По умолчанию этот prop будет работать с v-model, то есть, когда компонент используется, он может принимать привязку к v-модели из родительского компонента Автоматическая передача событий...” Директива v-model для элемента <input> привязывает значение input к полю ввода. Когда пользователь вводит значение input, v-model запускает автоматическую отправку события update:modelValue. Это обновляет родительский компонент новым значением

Макрос defineModel не только упрощает двустороннюю привязку данных, но и уменьшает размер нашего кода на целых 66%.

Служебная функция предоставляет несколько дополнительных возможностей, которые стоит изучить. Чтобы узнать о них больше, обратитесь к документации.

Обработка сложного управления состоянием с двусторонней привязкой

По мере роста приложения часто рекомендуется перенести управление состоянием в глобальное хранилище и использовать такой инструмент, как Pinia, для эффективного управления.

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

Хотя двусторонняя привязка данных изначально не предназначалась для сложного управления состоянием из-за сложности использования v-model даже для базовой обработки состояния, выпуск макроса defineModel в Vue 3.4 упростил разделение состояний.

Это позволяет разработчикам поддерживать двустороннюю привязку VUE без проблем, связанных с альтернативными методами.

Рассмотрим приложение для управления задачами в качестве примера:

A Vue.js task manager example showing a task list and task details, with options to edit the title and status of a selected task.

Â' 

Это приложение состоит из компонента TaskManager с двумя вложенными компонентами: TaskList и TaskDetails. Компонент TaskList отображает список задач из родительского компонента.

Когда пользователи выбирают задачу из списка, в компоненте TaskDetails отображаются подробные сведения о задаче. Когда пользователи редактируют выбранную задачу и сохраняют изменения, эти изменения должны быть отражены как в родительском компоненте, так и в компоненте TaskList.

Родительский компонент TaskManager выглядит следующим образом:

//TaskManager.vue

<template>
 <div>
   <h1>Task Manager</h1>
   <p v-if="selectedTask">
     Currently Editing: {{ selectedTask.title }}
   </p>

   <div class="container">
     <!-- Task list -->
     <TaskList
         v-model="tasks"
         v-model:selected="selectedTask" />

     <!-- Task details -->
     <TaskDetails
         v-model="selectedTask" />
   </div>
 </div>
</template>

<script setup>
import { ref } from 'vue';
import TaskList from '@/components/TaskList.vue';
import TaskDetails from '@/components/TaskDetail.vue';

// Initial task state
const tasks = ref([
 { title: 'Complete report', status: 'Pending' },
 { title: 'Review PR', status: 'In Progress' }
]);

// Selected task for editing
const selectedTask = ref();
</script>

Здесь состояния объявляются и передаются компонентам TaskList и TaskDetail с помощью привязок.

Далее, компонент TaskDetail:

//components/TaskDetail.vue

<template>
 <div v-if="selected">
   <h2>Task Details</h2>
   <label>
     Title:
     <input v-model="title" />
   </label>
   <label>
     Status:
     <select v-model="status">
       <option>Pending</option>
       <option>In Progress</option>
       <option>Completed</option>
     </select>
   </label>
   <div>
     <button @click="handleSave">Save</button>
     <button @click="handleCancel">Done</button>
   </div>
 </div>
 <p v-else>
   Select a task to edit
 </p>
</template>

<script setup>
import { ref, watch } from 'vue';

const selected = defineModel({
 required: true
});

const title = ref('');
const status = ref('');

// Sync local state with the selected task
watch(selected, (task) => {
 if (!task) return;
 title.value = task.title;
 status.value = task.status;
});

function handleSave() {
 if (!selected.value) return;
 selected.value.title = title.value;
 selected.value.status = status.value;
}

function handleCancel() {
 selected.value = undefined;
}
</script>

Этот компонент использует функцию просмотра для отслеживания изменений в выбранной задаче. При обнаружении изменений он копирует состояние в локальные переменные (заголовок и статус). Таким образом, он может изменять состояние без изменения исходного состояния до тех пор, пока пользователь не сохранит правку.

Компонент TaskList содержит список задач и функцию, которая добавляет новую задачу в состояние:

//components/TaskList.vue

<template>
 <div>
   <h2>Task List</h2>
   <ul>
     <li
         v-for="(task, index) in tasks"
         :key="index"
         :class="{ selected: selected === task }"
         @click="selected = task">
       {{ task.title }} - {{ task.status }}
     </li>
   </ul>
   <button @click="handleAddTask">Add Task</button>
 </div>
</template>

<script setup>
const tasks = defineModel({
 required: true
});

const selected = defineModel('selected', {
 required: true
});

function handleAddTask() {
 tasks.value.push({ title: 'New Task', status: 'Pending' });
}
</script>

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

A Vue.js task manager interface showing a task list with an option to add a task and a prompt to select a task to edit.

Â' 

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

Упрощение с помощью составных элементов

Составные элементы - это повторно используемые функции в Vue 3, которые инкапсулируют логику состояния для таких распространенных задач, как форматирование даты или конвертация валюты, которые могут потребоваться в различных частях приложения.

Компонуемые функции аналогичны перехватчикам в React, которые позволяют разделить логику для повторного использования в нескольких компонентах.

Мы можем использовать составные элементы для дальнейшего упрощения нашего кода, отделив логику состояний от компонентов. Для этого мы просто разделяем состояния наших компонентов на отдельные функции:

//composables/task.js

import {ref} from "vue";

export const useTask = () => {
   const tasks = ref([
       { title: 'Complete report', status: 'Pending' },
       { title: 'Review PR', status: 'In Progress' }
   ]);

   // Selected task for editing
   const selectedTask = ref();
   return {
       tasks,
       selectedTask
   };
};

Здесь мы выделили состояния task и selectedTask из компонента TaskManager во внешнюю функцию useTask() в составе task.js composable.

Теперь, если мы проведем рефакторинг компонента TaskManager, он будет выглядеть следующим образом:

//TaskManager.vue

<template>
 <div>
   <h1>Task Manager</h1>
   <p v-if="selectedTask">
     Currently Editing: {{ selectedTask.title }}
   </p>

   <div class="container">
     <!-- Task list -->
     <TaskList
         v-model="tasks"
         v-model:selected="selectedTask" />

     <!-- Task details -->
     <TaskDetails
         v-model="selectedTask" />
   </div>
 </div>
</template>

<script setup>
import TaskList from '@/components/TaskList.vue';
import TaskDetails from '@/components/TaskDetail.vue';
import {useTask} from "@/composables/task.js";

const {tasks, selectedTask} = useTask();
</script>

Мы просто импортируем только что созданный composable и деструктурируем из него состояния. Компонент функционирует как и раньше, но код стал чище.

Мы сделаем то же самое для компонента TaskDetail:

//composables/taskDetail.js

import { ref, watch } from 'vue';

export const useDetail = (selected) => {

   const title = ref('');
   const status = ref('');

   // Sync local state with the selected task
   watch(selected, (task) => {
       if (!task) return;
       title.value = task.title;
       status.value = task.status;
   });

   function handleSave() {
       if (!selected.value) return;
       selected.value.title = title.value;
       selected.value.status = status.value;
   }

   function handleCancel() {
       selected.value = undefined;
   }

   return {
       title,
       status,
       handleSave,
       handleCancel,
   };
};

Затем выполните рефакторинг компонента TaskDetail:

//components/TaskDetail.vue

<template>
 <div v-if="selected">
   <h2>Task Details</h2>
   <label>
     Title:
     <input v-model="title"/>
   </label>
   <label>
     Status:
     <select v-model="status">
       <option>Pending</option>
       <option>In Progress</option>
       <option>Completed</option>
     </select>
   </label>
   <div>
     <button @click="handleSave">Save</button>
     <button @click="handleCancel">Done</button>
   </div>
 </div>
 <p v-else>
   Select a task to edit
 </p>
</template>

<script setup>
import {useDetail} from "@/composables/taskDetail.js";

const selected = defineModel({
 required: true
});

const {
 handleCancel,
 handleSave,
 status,
 title
} = useDetail(selected);
</script>

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

Соображения по поводу производительности и лучшие практики

При работе с двусторонней привязкой данных в Vue, особенно в сложных приложениях, важно учитывать производительность и удобство обслуживания. Ниже приведены соображения по производительности и рекомендации, которым следует следовать при реализации двусторонней привязки данных:

Оптимизированное управление состоянием

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

Избегайте глубоких наблюдателей

Возможно, вам придется использовать встроенные средства наблюдения ({ deep: true }) для наблюдения за вложенными изменениями в объекте. Однако они могут быть дорогостоящими, поскольку должны охватывать все вложенные свойства. Рассмотрите возможность реструктуризации данных или использования целевых средств наблюдения.

Обеспечьте правильное обращение с опорой

При создании пользовательских компонентов с помощью v-model убедитесь, что генерируемые обновления обрабатываются должным образом, чтобы избежать бесконечных циклов или чрезмерного количества обновлений. Генерируйте события вручную для более точного управления, когда одной v-model недостаточно для сложных потоков данных.

Отделить логику состояния от пользовательского интерфейса

Используйте компонуемые функции, чтобы отделить бизнес-логику от пользовательского интерфейса компонента. Это уменьшит количество прямых привязок данных в шаблонах.

Вывод

В этой статье мы исследовали тонкости двусторонней привязки данных в Vue и продемонстрировали, как этот механизм функционирует в приложениях различной сложности. Мы особо выделили инструменты и функциональные возможности, такие как макрос defineModel и compositive API composables, которые значительно улучшают нашу работу с двусторонней привязкой данных.

Независимо от того, являетесь ли вы опытным разработчиком или новичком в Vue, двусторонняя привязка данных и связанные с ней инструменты помогут вам следовать рекомендациям и создавать более совершенные и управляемые приложения, не полагаясь на сторонние инструменты.