Недавно, сидя за уютным столиком в кафешке и разбирая рабочие моменты, решил отвлечься, взглянуть по сторонам и зайти в проблему с другого угла. Внезапно, моё внимание привлёк диалог, ведущийся за соседним через проход столиком справа. Два молодых человека обсуждали.. реактивность во 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() если в неё передать нереактивную переменную:
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.
Для динамической навигации, как уже было озвучено:
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 в этом кейсе даст какой то прирост чего бы там ни было.
