ref vs shallowRef
Недавно, сидя за уютным столиком в кафешке и разбирая рабочие моменты, решил отвлечься, взглянуть по сторонам и зайти в проблему с другого угла. Внезапно, моё внимание привлёк диалог, ведущийся за соседним через проход столиком справа. Два молодых человека обсуждали. реактивность во 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. Эта функция проверяет, есть ли в аргументах переданного объекта ® поле __v_isRef со значением true и возвращает булево значение этого факта — true или false.
Соответственно, если в функцию shallowRef () передать аргументом реактивную переменную, то shallowRef превращается в обыкновенный ref? (строка 8)
Давайте проверим!
{{ shallow }}
Нажимаем кнопку, и меняем внутреннее поле нашего 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 {
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 = (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() {
...
}
Почему? — vue3 сам по себе не пропустит в этом кейсе ref. В консоли вылезет жуткое предупреждение о том, что ты накосячил, и юзай shallowRef, чтобы не отслеживать внутренние поля компонента (коих весьма много).
2.Когда есть огромная структура данных, состоящая из многих объектов, часть или все из которых сами по себе являются ref. В этом случае в тех из них, которые не нужно отслеживать использовать shallowRef.
3. При запросе данных с бэкэнда.
Обычно данные с бэка обновляются единым массивом (или объектом) и нет нужны отслеживать у них внутренние изменения. Соответственно при каждом новом запросе по такому же url данные будут вновь прилетать таким же объектом, который перезапишет существующий.
4. В-принципе можно использовать вместо ref с примитивами, но тут нужно быть внимательным, не поменяется ли у нас примитив на объект, внутреннее состояние которого нужно отслеживать. Тут как говорится, типизация вам в помощь.
Последний юзкейс я бы отнёс к сомнительным, ибо не думаю, что замена ref на shallowRef в этом кейсе даст какой то прирост чего бы там ни было.