← Курс/Computed properties в Vue#203 из 257+30 XP

Computed properties в Vue

Что такое computed

**Computed properties** — это вычисляемые свойства, которые автоматически пересчитываются только тогда, когда изменились их реактивные зависимости.

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

const items = ref([
  { name: 'Яблоко', price: 50, count: 3 },
  { name: 'Банан', price: 30, count: 5 },
  { name: 'Апельсин', price: 70, count: 2 },
])

// Computed автоматически пересчитается при изменении items
const totalPrice = computed(() => {
  console.log('Пересчёт totalPrice...')
  return items.value.reduce((sum, item) => sum + item.price * item.count, 0)
})

console.log(totalPrice.value)  // 440
</script>

<template>
  <p>Итого: {{ totalPrice }} руб.</p>
  <!-- .value не нужен в шаблоне -->
</template>

Кэширование — главное отличие от methods

Computed **кэшируются**: результат вычисляется один раз и сохраняется. Повторные обращения возвращают кэшированное значение без вычислений.

Method вызывается **заново при каждом обращении**:

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

const count = ref(0)

// Computed — вычислится один раз и закэшируется
const doubleComputed = computed(() => {
  console.log('computed вычислился')
  return count.value * 2
})

// Method — вычисляется при каждом вызове
function doubleMethod() {
  console.log('method вызван')
  return count.value * 2
}
</script>

<template>
  <!-- Если вызвать несколько раз в шаблоне: -->
  <p>{{ doubleComputed }}</p>  <!-- computed вычислился — один раз -->
  <p>{{ doubleComputed }}</p>  <!-- из кэша, без повторного вычисления -->

  <p>{{ doubleMethod() }}</p>  <!-- method вызван -->
  <p>{{ doubleMethod() }}</p>  <!-- method вызван — снова! -->
</template>

Кэширование особенно важно для дорогостоящих вычислений.

Когда computed НЕ пересчитывается

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

// ПЛОХО — Date.now() не реактивен, computed никогда не обновится
const now = computed(() => Date.now())

// ХОРОШО — если нужно обновляемое время, используй watch + ref
const currentTime = ref(Date.now())
setInterval(() => { currentTime.value = Date.now() }, 1000)

Writable computed — computed с сеттером

По умолчанию computed только для чтения. Но можно создать computed с геттером и сеттером:

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

const firstName = ref('Алексей')
const lastName = ref('Иванов')

const fullName = computed({
  // Getter — вычисляет значение
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // Setter — позволяет "устанавливать" значение
  set(value) {
    const parts = value.split(' ')
    firstName.value = parts[0]
    lastName.value = parts[1] || ''
  }
})

console.log(fullName.value)       // 'Алексей Иванов'
fullName.value = 'Борис Петров'   // вызовет setter
console.log(firstName.value)      // 'Борис'
console.log(lastName.value)       // 'Петров'
</script>

Computed vs Methods vs Watch

| | Computed | Methods | Watch |

|---|---|---|---|

| Кэширование | Да | Нет | — |

| Возвращает значение | Да | Да | Нет |

| Реагирует на изменения | Да | — | Да |

| Побочные эффекты | Нельзя | Можно | Можно |

| Использование | Производные данные | Действия | Async-операции |

**Правило**: если нужно **получить производное значение** → computed. Если нужно **выполнить действие** при изменении → watch.

Примеры

Система computed с dependency tracking — отслеживание зависимостей и автоматический пересчёт

// Реализуем упрощённую систему computed с трекингом зависимостей.
// В Vue эта система значительно сложнее, но принцип тот же.

// Стек активных вычислений (для отслеживания зависимостей)
const effectStack = []

// Создать отслеживаемое значение (аналог ref)
function signal(value) {
  const subscribers = new Set()

  return {
    get() {
      // Если есть активное вычисление — подписываем его
      if (effectStack.length > 0) {
        subscribers.add(effectStack[effectStack.length - 1])
      }
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        value = newValue
        console.log(`[signal] изменился -> ${newValue}, уведомляем ${subscribers.size} подписчиков`)
        subscribers.forEach(fn => fn())
      }
    }
  }
}

// Создать computed значение
function computed(getter) {
  let cachedValue
  let isDirty = true
  const subscribers = new Set()

  function recompute() {
    isDirty = true
    subscribers.forEach(fn => fn())
  }

  return {
    get() {
      if (effectStack.length > 0) {
        subscribers.add(effectStack[effectStack.length - 1])
      }
      if (isDirty) {
        // Запускаем геттер, отслеживая зависимости
        effectStack.push(recompute)
        cachedValue = getter()
        effectStack.pop()
        isDirty = false
        console.log(`[computed] пересчитан -> ${cachedValue}`)
      } else {
        console.log(`[computed] из кэша -> ${cachedValue}`)
      }
      return cachedValue
    }
  }
}

// --- Демонстрация ---

const price = signal(100)
const quantity = signal(3)
const discount = signal(0)

// totalPrice зависит от price, quantity, discount
const totalPrice = computed(() => {
  const subtotal = price.get() * quantity.get()
  return subtotal - subtotal * (discount.get() / 100)
})

// discountedLabel зависит от totalPrice (цепочка computed!)
const label = computed(() => `Итого: ${totalPrice.get()} руб.`)

console.log('=== Первое чтение ===')
console.log(label.get())

console.log('\n=== Второе чтение (кэш) ===')
console.log(label.get())

console.log('\n=== Изменяем quantity ===')
quantity.set(5)
console.log(label.get())   // пересчёт cascades через зависимости

console.log('\n=== Изменяем discount ===')
discount.set(10)
console.log(label.get())