← Курс/readonly и markRaw: защита от изменений#215 из 257+20 XP

readonly и markRaw: защита от изменений

readonly(): неизменяемый реактивный объект

readonly() создаёт **только для чтения** Proxy над реактивным (или обычным) объектом. Любая попытка изменить его свойства вызовет предупреждение в режиме разработки:

import { reactive, readonly } from 'vue'

const state = reactive({ count: 0, name: 'Алексей' })
const readonlyState = readonly(state)

readonlyState.count = 5
// [Vue warn]: Set operation on key "count" failed: target is readonly.

console.log(readonlyState.count) // 0 — не изменилось

Важно: readonly не клонирует объект. Если исходный объект изменится, изменения будут видны через readonly-версию:

state.count = 10
console.log(readonlyState.count) // 10 — отражает изменения источника

Применение readonly: паттерн «только источник мутирует»

Классический паттерн — хранить состояние приватно, а наружу выдавать readonly-версию:

// store.js
import { reactive, readonly } from 'vue'

function createStore() {
  const state = reactive({ user: null, isLoading: false })

  function setUser(user) { state.user = user }
  function setLoading(v) { state.isLoading = v }

  // Компоненты могут только читать state, но не изменять напрямую
  return { state: readonly(state), setUser, setLoading }
}

readonly глубокая по умолчанию

Как и reactive, readonly работает рекурсивно — защищает все вложенные объекты:

const config = readonly({
  api: {
    url: 'https://api.example.com',
    timeout: 5000
  }
})

config.api.url = 'other'  // тоже выдаст предупреждение

Для поверхностной защиты есть shallowReadonly().

markRaw(): исключить объект из реактивности

markRaw() помечает объект специальным флагом, чтобы Vue **никогда не оборачивал его в Proxy**. Полезно для объектов, которые:

  • управляют своим DOM сами (Chart.js, Three.js рендерер)
  • содержат функции, не совместимые с Proxy
  • очень большие и не нуждаются в отслеживании
  • import { reactive, markRaw } from 'vue'
    
    // Экземпляр Chart.js — не нужна реактивность
    const chartInstance = markRaw(new Chart(canvas, options))
    
    const state = reactive({
      chart: chartInstance,  // Vue не обернёт в Proxy!
      data: [],
    })
    
    // state.chart — это тот же оригинальный объект Chart, не Proxy

    Проверочные функции

    Vue предоставляет функции для проверки типа объекта:

    import { reactive, readonly, isProxy, isReactive, isReadonly, isRef, ref } from 'vue'
    
    const raw = { a: 1 }
    const reactiveObj = reactive(raw)
    const readonlyObj = readonly(reactiveObj)
    const count = ref(0)
    
    isProxy(reactiveObj)    // true — это Proxy
    isProxy(readonlyObj)    // true — тоже Proxy
    isProxy(raw)            // false
    
    isReactive(reactiveObj) // true
    isReactive(readonlyObj) // true (readonly реактивного = реактивный readonly)
    isReadonly(readonlyObj) // true
    isReadonly(reactiveObj) // false
    
    isRef(count)            // true
    isRef(reactiveObj)      // false

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

  • readonly() — публичное API хранилища, конфигурация приложения, пропсы (Vue уже делает это для вас)
  • markRaw() — сторонние библиотеки с DOM-управлением, большие статические данные (словари, константы), классы с методами которые Proxy ломает
  • Примеры

    Реализация readonly через Proxy и markRaw через символ-флаг, демонстрация паттерна хранилища

    // Реализуем readonly и markRaw через Proxy и Symbol
    
    const RAW_MARK = Symbol('markRaw')
    const IS_READONLY = Symbol('isReadonly')
    
    // markRaw: помечаем объект флагом — Vue не будет его оборачивать
    function markRaw(obj) {
      Object.defineProperty(obj, RAW_MARK, { value: true, enumerable: false })
      return obj
    }
    
    function isMarkedRaw(obj) {
      return obj && obj[RAW_MARK] === true
    }
    
    // readonly: Proxy, который блокирует set
    function readonly(obj) {
      const proxy = new Proxy(obj, {
        get(target, key) {
          if (key === IS_READONLY) return true
          const value = target[key]
          // Рекурсивно оборачиваем вложенные объекты
          if (typeof value === 'object' && value !== null && !isMarkedRaw(value)) {
            return readonly(value)
          }
          return value
        },
        set(target, key) {
          console.warn(`[Vue warn] Set operation on key "${String(key)}" failed: target is readonly.`)
          return true  // Не кидаем ошибку, просто предупреждаем
        },
        deleteProperty(target, key) {
          console.warn(`[Vue warn] Delete operation on key "${String(key)}" failed: target is readonly.`)
          return true
        }
      })
      return proxy
    }
    
    function isReadonly(obj) {
      return !!(obj && obj[IS_READONLY])
    }
    
    // === Демонстрация readonly ===
    console.log('=== readonly(): защита от мутаций ===')
    
    const state = { count: 0, user: { name: 'Алексей', role: 'admin' } }
    const readonlyState = readonly(state)
    
    console.log('Чтение работает:')
    console.log('  readonlyState.count:', readonlyState.count)
    console.log('  readonlyState.user.name:', readonlyState.user.name)
    
    console.log('\nПопытки мутации:')
    readonlyState.count = 999        // предупреждение
    readonlyState.user.role = 'user' // предупреждение (вложенный объект тоже защищён)
    
    console.log('Значения не изменились:')
    console.log('  readonlyState.count:', readonlyState.count)   // 0
    console.log('  readonlyState.user.role:', readonlyState.user.role) // 'admin'
    
    // Но исходный объект можно изменить
    state.count = 42
    console.log('  После изменения state.count = 42:')
    console.log('  readonlyState.count:', readonlyState.count)   // 42
    
    console.log('\nisReadonly проверки:')
    console.log('  isReadonly(readonlyState):', isReadonly(readonlyState))  // true
    console.log('  isReadonly(state):', isReadonly(state))                  // false
    
    // === Демонстрация markRaw ===
    console.log('\n=== markRaw(): исключение из реактивности ===')
    
    // Симулируем библиотечный объект (Chart.js, Three.js и т.д.)
    class ChartLibrary {
      constructor(options) {
        this.options = options
        this.canvas = null
        console.log('  Chart создан')
      }
      update(data) {
        console.log('  Chart.update() вызван с:', JSON.stringify(data))
      }
    }
    
    const rawChart = markRaw(new ChartLibrary({ type: 'bar' }))
    
    console.log('isMarkedRaw(rawChart):', isMarkedRaw(rawChart))  // true
    
    // Внутри reactive-объекта markRaw-объект не оборачивается
    const appState = { chart: rawChart, data: [1, 2, 3] }
    const readonlyApp = readonly(appState)
    
    const chart = readonlyApp.chart
    console.log('Получили chart через readonly:', chart.constructor.name)
    chart.update({ values: [10, 20, 30] })  // работает без Proxy-обёртки
    
    // === Паттерн хранилища ===
    console.log('\n=== Паттерн: хранилище с readonly публичным API ===')
    
    function createUserStore() {
      const _state = { user: null, isLoading: false, error: null }
    
      return {
        state: readonly(_state),
    
        login(username) {
          _state.isLoading = true
          console.log(`  [store] Вход как "${username}"`)
          // Симулируем асинхронность
          _state.user = { name: username, id: Math.random().toString(36).slice(2) }
          _state.isLoading = false
        },
    
        logout() {
          _state.user = null
          console.log('  [store] Выход')
        }
      }
    }
    
    const store = createUserStore()
    console.log('user до login:', store.state.user)
    
    store.state.user = { name: 'Хакер' }  // предупреждение — нельзя!
    console.log('После попытки прямой мутации:', store.state.user)  // null
    
    store.login('Алексей')
    console.log('После store.login():', store.state.user)