Метка: Composition API

  • Vue 3 Slots передача параметров

    Slots (слоты) в Vue 3 предоставляют мощный механизм для создания гибких и переиспользуемых компонентов. В этой статье мы разберём все способы передачи параметров в слоты с примерами из реальной практики.

    Основные типы слотов в Vue 3

    • Слоты по умолчанию — базовый способ передачи контента
    • Именованные слоты — для точного позиционирования контента
    • Scoped slots — с передачей параметров из дочернего компонента

    1. Scoped Slots (Основной способ передачи параметров)

    <!-- Дочерний компонент -->
    <template>
      <div>
        <slot :item="item" :index="index"></slot>
      </div>
    </template>
    
    <!-- Родительский компонент -->
    <ChildComponent>
      <template v-slot:default="slotProps">
        {{ slotProps.item }} - {{ slotProps.index }}
      </template>
    </ChildComponent>

    2. Именованные scoped slots

    <!-- Дочерний компонент -->
    <template>
      <div>
        <slot name="header" :title="title"></slot>
        <slot name="content" :data="contentData"></slot>
      </div>
    </template>
    
    <!-- Родительский компонент -->
    <DataContainer>
      <template #header="{ title }">
        <h2>{{ title }}</h2>
      </template>
      
      <template #content="{ data }">
        <p>{{ data.description }}</p>
      </template>
    </DataContainer>

    3. Динамические параметры слотов

    <!-- Дочерний компонент -->
    <script setup>
    const slotProps = computed(() => ({
      user: currentUser.value,
      timestamp: new Date()
    }))
    </script>
    
    <template>
      <slot v-bind="slotProps"></slot>
    </template>

    4. Деструктуризация параметров

    <template #item="{ id, name }">
      <div>{{ id }}: {{ name }}</div>
    </template>

    Практические примеры

    Пример 1: Гибкий список

    <SmartList :items="users">
      <template #item="{ user }">
        <UserCard :user="user" />
      </template>
    </SmartList>

    Пример 2: Модальное окно с параметрами

    <ModalDialog>
      <template #header="{ close }">
        <button @click="close">×</button>
      </template>
      
      <template #default="{ data }">
        {{ data.message }}
      </template>
    </ModalDialog>

    Лучшие практики

    • Используйте осмысленные имена для параметров слотов
    • Для сложных компонентов документируйте структуру слотов
    • Избегайте глубокой вложенности scoped slots
    • Используйте TypeScript для типизации параметров

    Заключение

    Scoped slots в Vue 3 предоставляют мощный инструмент для создания гибких компонентов. Освоив передачу параметров в слоты, вы сможете создавать по-настоящему переиспользуемые UI-компоненты.

    Для более сложных сценариев рассмотрите использование Composition API вместе со слотами.

    Vue 3 Composition API + Slots: Сложные сценарии использования

    В сочетании с Composition API слоты Vue 3 раскрывают свою настоящую мощь. Рассмотрим продвинутые паттерны для сложных UI-компонентов.

    1. Динамические слоты с реактивными параметрами

    <!-- DynamicTable.vue -->
    <script setup>
    import { ref, computed } from 'vue'
    
    const props = defineProps(['data'])
    const sortDirection = ref('asc')
    
    const sortedData = computed(() => {
      return [...props.data].sort((a, b) => {
        return sortDirection.value === 'asc' 
          ? a.value - b.value 
          : b.value - a.value
      })
    })
    </script>
    
    <template>
      <table>
        <slot name="header" :toggleSort="() => sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'"></slot>
        <tr v-for="(item, index) in sortedData" :key="index">
          <slot :item="item" :index="index"/>
        </tr>
      </table>
    </template>

    Применение: Таблицы с сортировкой, где логика инкапсулирована в компоненте, а рендеринг контролируется через слоты.

    2. Состояние модальных окон

    <!-- useModal.js -->
    import { ref } from 'vue'
    
    export function useModal() {
      const isOpen = ref(false)
      
      const open = () => isOpen.value = true
      const close = () => isOpen.value = false
      
      return { isOpen, open, close }
    }
    
    <!-- ModalComponent.vue -->
    <script setup>
    import { useModal } from './useModal'
    const modal = useModal()
    </script>
    
    <template>
      <slot :open="modal.open" :close="modal.close" :isOpen="modal.isOpen"/>
    </template>

    Использование:

    <ModalComponent v-slot="{ open, close, isOpen }">
      <button @click="open">Открыть</button>
      
      <div v-if="isOpen" class="modal">
        <slot name="content"/>
        <button @click="close">Закрыть</button>
      </div>
    </ModalComponent>

    3. Композиционные слоты для форм

    <!-- useFormField.js -->
    import { ref, computed } from 'vue'
    
    export function useFormField(initialValue) {
      const value = ref(initialValue)
      const isValid = computed(() => value.value.length > 0)
      
      return { value, isValid }
    }
    
    <!-- FormField.vue -->
    <script setup>
    import { useFormField } from './useFormField'
    
    const props = defineProps(['initialValue'])
    const field = useFormField(props.initialValue)
    </script>
    
    <template>
      <div class="form-field" :class="{ invalid: !field.isValid }">
        <slot :value="field.value" :isValid="field.isValid"/>
      </div>
    </template>

    Использование с кастомным input:

    <FormField initialValue="" v-slot="{ value, isValid }">
      <input 
        v-model="value"
        :class="{ error: !isValid }"
        placeholder="Введите текст"
      >
      <span v-if="!isValid">Поле обязательно</span>
    </FormField>

    4. Сложные компоненты данных

    <!-- DataFetcher.vue -->
    <script setup>
    import { ref, onMounted } from 'vue'
    
    const props = defineProps(['url'])
    const data = ref(null)
    const error = ref(null)
    const isLoading = ref(false)
    
    onMounted(async () => {
      isLoading.value = true
      try {
        const response = await fetch(props.url)
        data.value = await response.json()
      } catch (err) {
        error.value = err
      } finally {
        isLoading.value = false
      }
    })
    </script>
    
    <template>
      <slot 
        :data="data" 
        :error="error" 
        :isLoading="isLoading"
        :reload="onMounted"
      />
    </template>

    Использование:

    <DataFetcher url="/api/users" v-slot="{ data, isLoading }">
      <div v-if="isLoading">Загрузка...</div>
      <UserList v-else :users="data" />
    </DataFetcher>

    5. Продвинутый пример: Компонент вкладок

    <!-- TabsContainer.vue -->
    <script setup>
    import { ref } from 'vue'
    
    const activeTab = ref(0)
    const tabs = ref([])
    
    const registerTab = (title) => {
      const id = tabs.value.length
      tabs.value.push({ id, title })
      return id
    }
    
    const setActiveTab = (id) => {
      activeTab.value = id
    }
    </script>
    
    <template>
      <div class="tabs-container">
        <div class="tabs-header">
          <button 
            v-for="tab in tabs" 
            @click="setActiveTab(tab.id)"
            :class="{ active: activeTab === tab.id }"
          >
            {{ tab.title }}
          </button>
        </div>
        
        <div class="tabs-content">
          <slot :activeTab="activeTab"/>
        </div>
      </div>
    </template>

    Использование:

    <TabsContainer v-slot="{ activeTab }">
      <template #default>
        <Tab :register="registerTab" title="Профиль">
          <div v-if="activeTab === 0">...</div>
        </Tab>
        
        <Tab :register="registerTab" title="Настройки">
          <div v-if="activeTab === 1">...</div>
        </Tab>
      </template>
    </TabsContainer>

    Заключение

    Сочетание Composition API и слотов позволяет создавать:

    • Полностью инкапсулированную логику компонентов
    • Максимально гибкие API для переиспользуемых компонентов
    • Сложные stateful-компоненты с простым интерфейсом
    • Легко тестируемые решения (бизнес-логика отделена от рендеринга)

  • Реактивность в Vue 3: watch, watchEffect и продвинутые техники

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

    Основы реактивности в Vue 3

    Vue 3 полностью переработал систему реактивности, используя JavaScript Proxy вместо Object.defineProperty. Это обеспечивает:

    • Поддержку работы с массивами и коллекциями
    • Более эффективное отслеживание изменений
    • Возможность создания «сырых» (raw) объектов без реактивности
    • Лучшую интеграцию с TypeScript

    Как работает реактивность

    import { reactive, effect } from 'vue'
    
    const state = reactive({
      count: 0
    })
    
    // Аналог watchEffect в системе реактивности
    effect(() => {
      console.log('Count:', state.count)
    })

    watch vs watchEffect: полное сравнение

    watchEffect

    • Автоматическое отслеживание зависимостей
    • Немедленный запуск при создании
    • Не предоставляет старые значения
    • Идеален для побочных эффектов
    const count = ref(0)
    
    watchEffect(() => {
      console.log(`Count changed: ${count.value}`)
    })

    watch

    • Явное указание источников
    • Ленивое выполнение (по умолчанию)
    • Предоставляет старые и новые значения
    • Подходит для сравнения состояний
    watch(count, (newVal, oldVal) => {
      console.log(`Count changed from ${oldVal} to ${newVal}`)
    })

    Продвинутые техники работы с реактивностью

    1. Контроль зависимостей

    const condition = ref(false)
    const a = ref(1)
    const b = ref(2)
    
    watchEffect(() => {
      // Только condition будет зависимостью
      if (condition.value) {
        console.log(a.value + b.value) // a и b не станут зависимостями
      }
    })

    2. Глубокое наблюдение с кастомным сравнением

    watch(
      () => ({ ...complexObject }),
      (newVal, oldVal) => {
        // Логика сравнения
      },
      {
        deep: true,
        equals: (a, b) => 
          a.id === b.id && a.items.length === b.items.length
      }
    )

    3. Реактивные цепочки и оптимизация

    const searchQuery = ref('')
    const filters = reactive({ 
      category: '', 
      sort: 'asc',
      // 10K элементов
      items: hugeArray 
    })
    
    // Оптимизированный watch
    watch(
      () => ({
        query: searchQuery.value,
        category: filters.category,
        sort: filters.sort
      }),
      ({ query, category, sort }) => {
        // Фильтрация без отслеживания hugeArray
        filterItems(query, category, sort)
      }
    )

    Практические примеры из реальных проектов

    1. Интеграция с API

    const pagination = reactive({
      page: 1,
      size: 20,
      total: 0
    })
    
    const fetchData = async () => {
      const res = await api.get('/items', {
        params: {
          page: pagination.page,
          size: pagination.size
        }
      })
      pagination.total = res.total
    }
    
    // Автоматический запрос при изменении пагинации
    watch([() => pagination.page, () => pagination.size], fetchData, {
      immediate: true
    })

    2. Управление состоянием формы

    const form = reactive({
      email: '',
      password: '',
      errors: {}
    })
    
    watch(
      () => form.email,
      (email) => {
        form.errors.email = validateEmail(email) 
          ? null 
          : 'Invalid email format'
      },
      { debounce: 300 }
    )

    Производительность и отладка

    1. Измерение времени выполнения

    watchEffect((onCleanup) => {
      const start = performance.now();
      
      // Тяжелая операция
      processLargeData();
      
      const duration = performance.now() - start;
      if (duration > 50) {
        console.warn(`Slow effect: ${duration.toFixed(2)}ms`);
      }
      
      onCleanup(() => {
        // Очистка ресурсов
      })
    })

    2. Визуализация зависимостей

    function trackDependencies(effect) {
      const deps = new Set()
      
      const reactiveEffect = watchEffect(() => {
        effect()
        console.log('Dependencies:', [...deps])
        deps.clear()
      }, {
        onTrack(e) {
          deps.add(e.target[e.key])
        }
      })
      
      return reactiveEffect
    }

    Заключение и лучшие практики

    • Используйте watch когда нужны старые значения или точный контроль
    • Выбирайте watchEffect для автоматического отслеживания зависимостей
    • Оптимизируйте глубокое наблюдение с помощью кастомных функций сравнения
    • Разделяйте сложные эффекты на несколько простых watchers
    • Всегда очищайте ресурсы в onCleanup
    • Мониторьте производительность сложных эффектов

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