← Vue 3/shallowRef и shallowReactive: поверхностная реактивность#320 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: React или VueПрактика: Vue setТермин: Composition APIТема: Vue 3 и Composition API
← НазадДалее →

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 — подписчик уведомлён')

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 — подписчик уведомлён')

Задание

Создай демо, показывающее разницу между reactive и shallowReactive. Используй shallowRef для большого массива данных и triggerRef для принудительного обновления. При клике на "Изменить вложенные данные" UI должен обновиться только после triggerRef.

Подсказка

Замени первый ??? на shallowRef, второй на shallowReactive, третий на triggerRef(items). shallowRef отслеживает только .value, а не содержимое массива. shallowReactive отслеживает только корневые свойства объекта.

Загружаем среду выполнения...
Загружаем AI-помощника...