Недавно, сидя за уютным столиком в кафешке и разбирая рабочие моменты, решил отвлечься, взглянуть по сторонам и зайти в проблему с другого угла. Внезапно, моё внимание привлёк диалог, ведущийся за соседним через проход столиком справа. Два молодых человека обсуждали.. реактивность во vue3. Судя по всему, я попал на часть своеобразного собеседования, проводившегося в камерной атмосфере этого заведения.

Так, с ref понятно, а что ты мне можешь рассказать про shallowRef ?

Интервьюер дежурной улыбкой подбадривал соискателя, а взглядом оценивал девицу, несущую кофе.

Ну.. ээ.. shallowRef это облегченная версия ref, которая не имеет глубокой реактивности.

А расскажи, где её применяют и вообще зачем она нужна, если есть ref ?

Я честно говоря не применял эту штуку нигде кроме динамических компонентов, потому что моя среда разработки выбрасывает ошибку, когда я использую в этом кейсе ref.

Давайте оставим этих двоих заниматься их делами и перейдём к сути статьи:

Чем же отличается Ref от ShallowRef ?

Рассмотрим shallowRef:

Не буду копировать строки из документации, их любой может прочитать. Давайте залезем поглубже и посмотрим на детали реализации.

https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts#L148

Репозиторий кода vue3 на github.

export function shallowRef(value?: unknown) {
  return createRef(value, true)
}


function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}


export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

Анализируем: функция shallowRef опционально принимает аргумент, и возвращает другую функцию createRef, с параметром shallow = true. По сути это просто обёртка, которая не делает ничего, кроме вызова другой функции с жестко заданным параметром.

Взглянем на createRef функцию и конкретно на внутреннюю проверку на isRef. Эта функция проверяет, есть ли в аргументах переданного объекта (r) поле __v_isRef со значением true и возвращает булево значение этого факта - true или false.

Соответственно, если в функцию shallowRef() передать аргументом реактивную переменную, то shallowRef превращается в обыкновенный ref ? (строка 8)

Давайте проверим!

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

const trueRef = ref({name: 'Max'});
const shallow = shallowRef(trueRef);

</script>

<template>
<div>
  <span>{{ shallow }}</span>
</div>
<button @click="shallow.name = 'Jake'">Button</button>
</template>
Нажимаем кнопку, и меняем внутреннее поле нашего shallowRef
Нажимаем кнопку, и меняем внутреннее поле нашего shallowRef

Ой, а как же так? Мы изменили вложенное значение в объекте, в котором не должно отслеживаться изменение внутренних полей?

Это и есть нюанс - shallowRef не отслеживает изменение внутренних полей объекта, при условии, что сам объект не является реактивной переменной!

Теперь посмотрим, что возвращает функция shallowRef() если в неё передать нереактивную переменную:

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

Что это за new RefImpl ? (напомню, параметр shallow у нас сейчас true)

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

Ага, это экземпляр класса, содержащий в себе некоторые поля и конструктор, который мы и заюзали, передав два аргумента - собственно значение, переданное в функцию и переменную shallow = true.

В самом конструкторе мы инициализируем поля класса:

this._rawValue = __v_isShallow ? value : toRaw(value)

this._value = __v_isShallow ? value : toReactive(value)

Так как переданное в конструктор булево значение у нас true ( то есть shallow = true ) - мы присваиваем полям _rawValue и _value оригинальное значение, переданное в конструктор.

Если же shallow = false (как происходит при создании ref переменной), то полю _rawValue присваивается оригинальное значение объекта (нереактивное), а поле _value оборачивается в функцию toReactive()

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

которая возвращает реактивный прокси для объекта, или само значение, если в функцию передан не объект.

Подытожим:
Отличие ref от shallowRef именно в способе создания экземпляра класса RefImpl, который и создаёт нам объект, возвращаемый функциями ref() и shallowRef().

В случае с ref возвращается объект класса, содержащий в поле .value реактивный прокси, отслеживающий все изменения внутреннего состояния объекта, а в случае shallowRef возвращается объект класса, содержащий в поле .value оригинальное значение, переданное в функцию создания.

Для примитивов разницы нет - ref или shallowRef будут работать одинаково.

C одним исключением - shallowRef будет отслеживать внутренние изменения полей объекта, если передать ей реактивную переменную в качестве аргумента.

Теперь рассмотрим юзкейсы:

Автор не претендует на абсолютное перечисление всех юзкейсов, однако, приведет некоторые примеры, которые позволят сделать выводы о том, где и для чего можно использовать shallowRef.

  1. Для динамической навигации, как уже было озвучено:

import TextComponent from "./src/components/TextComponent.vue"
import NumberComponent from "./src/components/NumberComponent.vue"

const currentComponent = shallowRef(NumberComponent);

function changeComponent() {
 ...
}

<template>
  <component :is="currentComponent"></component>
  <button @click="changeComponent">Change</button>
</template>

Почему? - vue3 сам по себе не пропустит в этом кейсе ref. В консоли вылезет жуткое предупреждение о том, что ты накосячил, и юзай shallowRef, чтобы не отслеживать внутренние поля компонента (коих весьма много).

2.Когда есть огромная структура данных, состоящая из многих объектов, часть или все из которых сами по себе являются ref. В этом случае в тех из них, которые не нужно отслеживать использовать shallowRef.

3. При запросе данных с бэкэнда.
Обычно данные с бэка обновляются единым массивом (или объектом) и нет нужны отслеживать у них внутренние изменения. Соответственно при каждом новом запросе по такому же url данные будут вновь прилетать таким же объектом, который перезапишет существующий.

4. В-принципе можно использовать вместо ref с примитивами, но тут нужно быть внимательным, не поменяется ли у нас примитив на объект, внутреннее состояние которого нужно отслеживать. Тут как говорится, типизация вам в помощь.

Последний юзкейс я бы отнёс к сомнительным, ибо не думаю, что замена ref на shallowRef в этом кейсе даст какой то прирост чего бы там ни было.

Комментарии (2)