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. Эта функция проверяет, есть ли в аргументах переданного объекта (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 в этом кейсе даст какой то прирост чего бы там ни было.


ссылка на оригинал статьи https://habr.com/ru/articles/801349/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *