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