Hack Frontend Community

Computed, Methods и Watchers в Vue.js

Введение

В Vue есть три способа работы с реактивными данными: computed, methods и watchers. Каждый имеет свое назначение и особенности.


Computed Properties (Вычисляемые свойства)

Computed — это свойства, которые вычисляются на основе других реактивных данных и кэшируются.

Composition API

<template>
  <p>Имя: {{ firstName }}</p>
  <p>Фамилия: {{ lastName }}</p>
  <p>Полное имя: {{ fullName }}</p>
</template>

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

const firstName = ref('Иван')
const lastName = ref('Иванов')

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})
</script>

Options API

<script>
export default {
  data() {
    return {
      firstName: 'Иван',
      lastName: 'Иванов'
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}
</script>

Особенности computed

Кэширование

Результат вычисляется только при изменении зависимостей:

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

const count = ref(0)

const doubleCount = computed(() => {
  console.log('Вычисление...') // Вызовется только при изменении count
  return count.value * 2
})
</script>

<template>
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>
  <p>{{ doubleCount }}</p>
  <!-- console.log вызовется только один раз -->
</template>

Writable Computed (запись)

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

const firstName = ref('Иван')
const lastName = ref('Иванов')

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

// Использование
fullName.value = 'Петр Петров'
// firstName.value = 'Петр'
// lastName.value = 'Петров'
</script>

Methods (Методы)

Methods — это функции компонента, которые выполняются каждый раз при вызове.

Composition API

<template>
  <p>{{ getFullName() }}</p>
  <button @click="greet">Поздороваться</button>
</template>

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

const firstName = ref('Иван')
const lastName = ref('Иванов')

function getFullName() {
  console.log('Метод вызван')
  return `${firstName.value} ${lastName.value}`
}

function greet() {
  alert(`Привет, ${getFullName()}!`)
}
</script>

Options API

<script>
export default {
  data() {
    return {
      firstName: 'Иван',
      lastName: 'Иванов'
    }
  },
  methods: {
    getFullName() {
      return `${this.firstName} ${this.lastName}`
    },
    greet() {
      alert(`Привет, ${this.getFullName()}!`)
    }
  }
}
</script>

Особенности methods

Нет кэширования

<template>
  <p>{{ getFullName() }}</p>
  <p>{{ getFullName() }}</p>
  <p>{{ getFullName() }}</p>
  <!-- console.log вызовется 3 раза -->
</template>

<script setup>
function getFullName() {
  console.log('Метод вызван')
  return `${firstName.value} ${lastName.value}`
}
</script>

Принимают аргументы

<template>
  <p>{{ formatPrice(100) }}</p>
  <p>{{ formatPrice(250, 'USD') }}</p>
</template>

<script setup>
function formatPrice(price, currency = 'RUB') {
  return `${price} ${currency}`
}
</script>

Watchers (Наблюдатели)

Watchers — это функции, которые реагируют на изменения данных и выполняют побочные эффекты.

Composition API

<template>
  <input v-model="searchQuery" placeholder="Поиск..." />
</template>

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

const searchQuery = ref('')

watch(searchQuery, (newValue, oldValue) => {
  console.log(`Изменилось с "${oldValue}" на "${newValue}"`)
  // Выполнение запроса к API
  fetchResults(newValue)
})

function fetchResults(query) {
  // API запрос
}
</script>

Options API

<script>
export default {
  data() {
    return {
      searchQuery: ''
    }
  },
  watch: {
    searchQuery(newValue, oldValue) {
      console.log(`Изменилось с "${oldValue}" на "${newValue}"`)
      this.fetchResults(newValue)
    }
  },
  methods: {
    fetchResults(query) {
      // API запрос
    }
  }
}
</script>

Особенности watchers

Наблюдение за несколькими источниками

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

const firstName = ref('')
const lastName = ref('')

watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(`Полное имя: ${newFirst} ${newLast}`)
})
</script>

Немедленное выполнение

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

const count = ref(0)

watch(count, (newValue) => {
  console.log('Count изменился:', newValue)
}, { immediate: true })
// Вызовется сразу при создании
</script>

Глубокое наблюдение

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

const user = reactive({
  name: 'Иван',
  address: {
    city: 'Москва'
  }
})

watch(user, (newValue) => {
  console.log('User изменился:', newValue)
}, { deep: true })

// Сработает даже при изменении user.address.city
user.address.city = 'Санкт-Петербург'
</script>

watchEffect

Автоматически отслеживает зависимости:

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

const count = ref(0)
const doubled = ref(0)

watchEffect(() => {
  doubled.value = count.value * 2
  console.log(`Count: ${count.value}, Doubled: ${doubled.value}`)
})
// Автоматически отслеживает count
</script>

Сравнение

Computed vs Methods

СвойствоComputedMethods
КэшированиеДаНет
РеактивностьАвтоматическаяРучная
АргументыНетДа
Использование в template{{ computed }}{{ method() }}
Побочные эффектыНе рекомендуетсяМожно

Когда использовать computed:

  • Вычисление на основе других данных
  • Нужно кэширование
  • Синхронные операции

Когда использовать methods:

  • Обработка событий
  • Нужны параметры
  • Побочные эффекты (API запросы, изменение данных)

Computed vs Watchers

СвойствоComputedWatchers
ЦельВычисление значенияПобочные эффекты
РезультатВозвращает значениеНе возвращает
ЗависимостиАвтоматическиеЯвные
АсинхронностьНетДа

Когда использовать computed:

  • Нужно вычислить новое значение
  • Зависимости определяются автоматически
  • Синхронные операции

Когда использовать watchers:

  • API запросы
  • Работа с localStorage
  • Сложная асинхронная логика
  • Нужен доступ к старому и новому значению

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

Фильтрация списка (computed)

<template>
  <input v-model="searchQuery" placeholder="Поиск..." />
  <ul>
    <li v-for="user in filteredUsers" :key="user.id">
      {{ user.name }}
    </li>
  </ul>
</template>

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

const searchQuery = ref('')
const users = ref([
  { id: 1, name: 'Иван' },
  { id: 2, name: 'Петр' },
  { id: 3, name: 'Мария' }
])

const filteredUsers = computed(() => {
  return users.value.filter(user =>
    user.name.toLowerCase().includes(searchQuery.value.toLowerCase())
  )
})
</script>

API запрос (watcher)

<template>
  <input v-model="userId" type="number" placeholder="User ID" />
  <div v-if="loading">Загрузка...</div>
  <div v-else-if="userData">{{ userData.name }}</div>
</template>

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

const userId = ref(1)
const userData = ref(null)
const loading = ref(false)

watch(userId, async (newId) => {
  loading.value = true
  try {
    const response = await fetch(`/api/users/${newId}`)
    userData.value = await response.json()
  } finally {
    loading.value = false
  }
})
</script>

Форматирование (method с аргументами)

<template>
  <div v-for="product in products" :key="product.id">
    <p>{{ product.name }}</p>
    <p>{{ formatPrice(product.price) }}</p>
    <p>{{ formatPrice(product.price, 'USD') }}</p>
  </div>
</template>

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

const products = ref([
  { id: 1, name: 'Товар 1', price: 100 },
  { id: 2, name: 'Товар 2', price: 250 }
])

function formatPrice(price, currency = 'RUB') {
  return new Intl.NumberFormat('ru-RU', {
    style: 'currency',
    currency
  }).format(price)
}
</script>

Сохранение в localStorage (watcher)

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

const settings = ref({
  theme: 'light',
  language: 'ru'
})

// Загрузка из localStorage
const saved = localStorage.getItem('settings')
if (saved) {
  settings.value = JSON.parse(saved)
}

// Сохранение при изменении
watch(settings, (newSettings) => {
  localStorage.setItem('settings', JSON.stringify(newSettings))
}, { deep: true })
</script>

Частые ошибки

Использование methods вместо computed

<!-- Неэффективно -->
<template>
  <p>{{ getFullName() }}</p>
  <p>{{ getFullName() }}</p>
  <!-- Вызовется дважды -->
</template>

<!-- Эффективно -->
<template>
  <p>{{ fullName }}</p>
  <p>{{ fullName }}</p>
  <!-- Вычислится один раз -->
</template>

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

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})
</script>

Побочные эффекты в computed

<!-- Неправильно -->
<script setup>
import { computed, ref } from 'vue'

const count = ref(0)
const doubled = ref(0)

const bad = computed(() => {
  doubled.value = count.value * 2 // Побочный эффект!
  return count.value * 2
})
</script>

<!-- Правильно - используйте watcher -->
<script setup>
import { watch, ref } from 'vue'

const count = ref(0)
const doubled = ref(0)

watch(count, (newCount) => {
  doubled.value = newCount * 2
})
</script>

Синхронные операции в watcher

<!-- Неэффективно -->
<script setup>
const firstName = ref('')
const lastName = ref('')
const fullName = ref('')

watch([firstName, lastName], ([first, last]) => {
  fullName.value = `${first} ${last}`
})
</script>

<!-- Эффективно - используйте computed -->
<script setup>
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})
</script>

Вывод

Computed:

  • Вычисление значений на основе других данных
  • Автоматическое кэширование
  • Синхронные операции

Methods:

  • Обработка событий
  • Функции с параметрами
  • Побочные эффекты

Watchers:

  • Реакция на изменения
  • Асинхронные операции
  • Побочные эффекты (API, localStorage)

На собеседовании:

Важно уметь:

  • Объяснить разницу между computed, methods и watchers
  • Рассказать о кэшировании в computed
  • Привести примеры, когда использовать каждый вариант
  • Объяснить проблемы производительности при неправильном выборе