← Курс/shallowRef и shallowReactive: поверхностная реактивность#214 из 257+25 XP

shallowRef и shallowReactive: поверхностная реактивность

Глубокая реактивность по умолчанию

По умолчанию ref() и reactive() создают **глубокую (deep) реактивность**: Vue отслеживает изменения на всех уровнях вложенности объекта.

const state = reactive({
  user: {
    profile: {
      settings: {
        theme: 'dark'
      }
    }
  }
})

// Vue отслеживает изменение даже на 4-м уровне
state.user.profile.settings.theme = 'light'  // вызовет ре-рендер

Для этого Vue рекурсивно оборачивает каждый вложенный объект в Proxy. Это удобно, но может быть **дорогостоящим** для больших структур данных.

shallowRef: только верхний уровень

shallowRef() отслеживает только замену самого значения (изменение .value), но не изменения внутри объекта:

import { shallowRef } from 'vue'

const state = shallowRef({ count: 0, nested: { value: 'a' } })

// Это НЕ вызовет ре-рендер — внутренние изменения не отслеживаются
state.value.count = 1
state.value.nested.value = 'b'

// Это вызовет ре-рендер — мы меняем само .value
state.value = { count: 1, nested: { value: 'b' } }

shallowReactive: только корневые свойства

shallowReactive() делает реактивными только свойства первого уровня объекта:

import { shallowReactive } from 'vue'

const state = shallowReactive({
  name: 'Алексей',
  config: {
    theme: 'dark',
    language: 'ru'
  }
})

// Это вызовет ре-рендер — корневое свойство
state.name = 'Борис'

// Это НЕ вызовет ре-рендер — вложенный объект
state.config.theme = 'light'  // Vue не отслеживает это

// Замена всего объекта — вызовет ре-рендер
state.config = { theme: 'light', language: 'en' }

triggerRef: принудительное обновление

Если вы изменили внутренние данные shallowRef, но хотите принудительно запустить обновление, используйте triggerRef():

import { shallowRef, triggerRef } from 'vue'

const list = shallowRef([1, 2, 3])

// Мутируем массив напрямую (не заменяем .value)
list.value.push(4)
// DOM не обновится автоматически

triggerRef(list)  // принудительно запускаем обновление

Когда использовать shallow-варианты

Используй `shallowRef` / `shallowReactive` когда:

1. **Большие неизменяемые структуры** — например, список из тысяч записей, который всегда заменяется целиком (данные с API).

2. **Внешние объекты без реактивности** — библиотечные объекты (Chart.js, Three.js, Mapbox), которые управляют собой сами. Vue не должен их оборачивать в Proxy.

3. **Нормализованное хранилище** — когда вы сами управляете обновлениями и знаете, что нужно обновить.

// Хороший кейс для shallowRef: данные с сервера
const users = shallowRef([])

async function fetchUsers() {
  const data = await api.getUsers()
  users.value = data  // заменяем целиком — один ре-рендер
}
// Хороший кейс для shallowReactive: компонент с библиотечным объектом
const chartState = shallowReactive({
  chart: null,  // экземпляр Chart.js — не нужна глубокая реактивность
  data: [],
  isLoading: false
})

Примеры

Сравнение глубокой и поверхностной реактивности через Proxy — что отслеживается, а что нет

// Сравниваем deep reactive и shallow reactive

// Глубокая реактивность — рекурсивно оборачивает вложенные объекты
function deepReactive(obj, onChange, path = '') {
  if (typeof obj !== 'object' || obj === null) return obj

  // Рекурсивно оборачиваем все вложенные объекты
  for (const key of Object.keys(obj)) {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      obj[key] = deepReactive(obj[key], onChange, path ? `${path}.${key}` : key)
    }
  }

  return new Proxy(obj, {
    set(target, key, value) {
      const fullPath = path ? `${path}.${String(key)}` : String(key)
      const oldVal = target[key]
      if (typeof value === 'object' && value !== null) {
        value = deepReactive(value, onChange, fullPath)
      }
      target[key] = value
      onChange(fullPath, oldVal, value)
      return true
    }
  })
}

// Поверхностная реактивность — только первый уровень
function shallowReactive(obj, onChange) {
  return new Proxy(obj, {
    set(target, key, value) {
      const oldVal = target[key]
      target[key] = value
      onChange(String(key), oldVal, value)
      return true
    }
    // Вложенные объекты НЕ оборачиваются — только первый уровень
  })
}

const log = (label) => (path, oldVal, newVal) => {
  console.log(`  [${label}] "${path}" изменено: ${JSON.stringify(oldVal)} -> ${JSON.stringify(newVal)}`)
}

// === Тест глубокой реактивности ===
console.log('=== deepReactive (как reactive()) ===')
const deepState = deepReactive({
  name: 'Алексей',
  address: {
    city: 'Москва',
    coords: { lat: 55.7, lng: 37.6 }
  }
}, log('deep'))

deepState.name = 'Борис'                       // отслеживается
deepState.address.city = 'Питер'               // отслеживается
deepState.address.coords.lat = 59.9            // отслеживается (4-й уровень!)

// === Тест поверхностной реактивности ===
console.log('\n=== shallowReactive() ===')
const shallowState = shallowReactive({
  name: 'Алексей',
  address: {
    city: 'Москва',
    coords: { lat: 55.7, lng: 37.6 }
  }
}, log('shallow'))

shallowState.name = 'Борис'             // отслеживается (корневое свойство)
shallowState.address.city = 'Питер'     // НЕ отслеживается — вложено
shallowState.address.coords.lat = 59.9  // НЕ отслеживается — вложено

// Но замена всего объекта — отслеживается
shallowState.address = { city: 'Питер', coords: { lat: 59.9, lng: 30.3 } }

// === triggerRef эмуляция ===
console.log('\n=== Эмуляция shallowRef + triggerRef ===')
function createShallowRef(initial) {
  let _value = initial
  const subscribers = new Set()

  const ref = {
    get value() { return _value },
    set value(newVal) {
      _value = newVal
      this._notify()
    },
    _notify() {
      console.log(`  [shallowRef] Обновление! Текущее значение: ${JSON.stringify(_value)}`)
      subscribers.forEach(fn => fn(_value))
    },
    subscribe(fn) { subscribers.add(fn) }
  }
  return ref
}

function triggerRef(ref) {
  ref._notify()
}

const list = createShallowRef([1, 2, 3])
list.subscribe(v => console.log(`  [subscriber] Список: [${v.join(', ')}]`))

list.value.push(4)  // мутация — подписчик не вызывается
console.log('После push(4) без triggerRef — подписчик молчит')
console.log('Но данные изменились:', list.value)

triggerRef(list)  // принудительно уведомляем
console.log('После triggerRef — подписчик уведомлён')