[Перевод] Vue 3 Composition API: Ref или Reactive

exj3xowucgtjuq4f5lvbpeodrd4.png

Сейчас, когда я пишу эту статью, мы все ближе к релизу Vue 3. На мой взгляд самое интересное наблюдать как воспримут и будут применять его другие разработчики. У меня была возможность поиграть с Vue 3 в последние несколько месяцев, но я знаю есть те, у кого такой возможности не было.

Самое значительное нововведение в новой версии это Composition API. Он дает альтернативный подход к созданию компонентов и очень отличается от существующего Options API. Мне не сложно признать, что когда я впервые увидел его, я его не понял. Однако, по мере применения начал появляться смысл. Может вы не будете переписывать все ваше приложение с использованием Composition API, но статья даст вам возможность подумать как будет происходить создание компонентов и использование композиции.

Я провел пару презентаций недавно, и частым вопросом был: когда я использую Ref, а когда Reactive, для объявления реактивного свойства. У меня не было хорошего ответа, и я посвятил пару недель нахождению ответа, и эта статья результат моих исследований.

Хотел бы отметить, к тому же, что изложенное — мое мнение и пожалуйста не думайте, что надо делать «только так, и не иначе». Я планирую так использовать Ref и Reactive, пока кто-нибудь не посоветует другое, или пока я сам не найду лучший подход. Я думаю, для понимания любой новой технологии требуется какое-то время, а следом может появится и отработанные методики.

Перед тем как приступить, я допускаю, что вы хотя бы вкратце ознакомились с composition API и понимаете из чего он состоит. Статья фокусируется на различиях ref и reactive, а не на механизме composition API.

Реактивность Vue 2


Чтобы немного ввести вас в курс дела, я вкратце рассмотрю как создать реактивные свойства в приложении на Vue 2. Когда вы хотите, чтобы Vue 2 отслеживал изменения в свойствах, вам надо задекларировать их в объекте, который возвращается функцией data.





Под капотом Vue 2 для каждого свойства вызывается Object.defineProperty () создающий геттер и сеттер, чтобы отслеживать изменения. Это простейшее объяснение процесса и я хочу донести мысль: в этом нет магии. Вы не можете создать реактивные свойства где попало и ожидать, что Vue будет отслеживать изменения в них. Вам необходимо задавать реактивные свойства в функции data.

REF и REACTIVE


При работе с Options API нам необходимо следовать некоторым правилам при декларировании реактивных свойств, тоже самое и при работе с Composition API. Вы не можете только создать свойство и ожидать реактивности. В следующем примере я задекларировал свойство title и функция setup () возвращает его, тем самым делая его доступным для шаблона.



Это сработает, но свойство title не будет реактивным. Т.е. если кто-то изменит title, эти изменения НЕ отразятся в Доме. Например, если вы измените title через 5 секунд, код ниже НЕ поменяет Дом.



Мы можем импортировать ref и использовать ее, чтобы сделать свойство реактивным. Под капотом Vue 3 создаст Proxy.



Хочу пояснить, что говоря о Ref и Reactive, мне кажется, есть два разных случая. Первый — создание компонента как в примере выше и вам нужны реактивные свойства. Второй — когда вы создаете функции позволяющие композицию, чтобы использовать их в компонентах и функциях. В статье мы обсудим оба сценария.

REF


При создании свойств простых типов, ref () ваш первый выбор. Это не серебряная пуля, но с это стоит начать. Напомню вам семь примитивных типов в JavaScript:

  • String
  • Number
  • BigInt
  • Boolean
  • Symbol
  • Null
  • Undefined
import { ref } from "vue";

export default {
  setup() {
    const title = ref("");
    const one = ref(1);
    const isValid = ref(true);
    const foo = ref(null);
  }
};

В предидущем примере у нас title имеет тип String, значит чтобы сделать свойство реактивным выбираем ref (). Если код ниже вызывает у вас некоторые вопросы не беспокойтесь, у меня были такие же вопросы.

import { ref } from "vue";

export default {
  setup() {
    const title = ref("Hello, Vue 3!");

    setTimeout(() => {
      title.value = "New Title";
    }, 5000);

    return { title };
  }
};

Почему мы используем const, если title будет меняться? Почему не использовать let? Если вы выведете title в консоль, вы могли бы ожидать увидеть Hello, Vue 3!, но вместо это отобразится объект:

{_isRef: true}
value: (...)
_isRef: true
get value: ƒ value()
set value: ƒ value(newVal)
__proto__: Object

Функция ref () примет аргумент и вернет реактивный и изменяемый ref объект. Ref объект имеет одно свойство value, ссылающееся на аргумент. Это означает, что если вы хотите получить или изменить значение надо будет использовать title.value, а так как это объект, который не будет меняться, я и объявил его const.

Обращение к REF


Следующий вопрос —, а почему в шаблоне мы не обращаемся title.value?

Когда Ref возвращается как свойство в контекст отрисовки (объект возвращаемый функцией setup ()) и идет обращение к нему в шаблоне, Ref автоматически возвращает value. В шаблоне добавлять .value не нужно.

Вычисляемые свойства — computed properties — работают так же, внутри функции setup () обращаться к ним так .value.

REACTIVE


Мы только что рассмотрели применение ref () для задания реактивности свойств простых типов. А что будет, если мы хотим создать реактивный объект? Мы по прежнему могли бы использовать ref (), но под капотом Vue будет использовать reactive (), так что я буду придерживаться использования reactive ().

С другой стороны reactive () не сработает с примитивными типами. Функция reactive () принимает объект и возвращает реактивный прокси оригинала. Это эквивалентно .observable () во Vue 2, и это имя функции было изменено во избежание путаницы с observables в RxJS.

import { reactive } from "vue";

export default {
  setup() {
    const data = reactive({
      title: "Hello, Vue 3"
    });

    return { data };
  }
};

Главное отличие в том, как мы обращаемся к реактивному объекту в шаблоне. В предидущем примере data это объект содержащий свойство title. Вам надо будет обращаться к нему в шаблоне так — data.title:



Разница между REF и REACTIVE в компоненте


Исходя из того, что мы обсудили, ответ казалось бы напрашивается? Используем ref () для простых типов, и reactive () для объектов. Когда я начал создавать компоненты, выяснилось, что это всегда не так, и документации говорится:

Разница между применением ref и reactive может быть, в какой-то мере, сравнима с тем, как вы пишете стандартную логику программы на JavaScript.

Я размышлял над этой фразой и пришел к следующим выводам. По мере роста приложения у меня появились следующие свойства:

export default {
  setup() {
    const title = ref("Hello, World!");
    const description = ref("");
    const content = ref("Hello world");
    const wordCount = computed(() => content.value.length);

    return { title, description, content, wordCount };
  }
};

В JavaScript я бы понял, что это все свойства моей страницы. В этом случае я бы сгруппировал бы их в объект page, почему бы не сделать тоже самое сейчас.



Это мой подход к вопросу Ref или Reactive, но было бы не плохо узнать ваше мнение. Делаете-ли вы также? Может это не правильный подход? Пожалуйста комментируйте.

Логика композиции


Вы не сможете ошибиться при использовании ref () или reactive () в ваших компонентах. Обе функции создадут реактивные данные, и если вы понимаете как к ним обращаться в вашей функции setup () и в шаблонах не возникнет каких-либо сложностей.

Когда же вы начнете писать композиционные функции вам надо будет понимать разницу. Я использую примеры из RFC документации, так как одни хорошо иллюстрируют нюансы.

Вам надо создать логику отслеживания положения мыши. Вы также надо иметь возможность использовать эту-же логику в любом компоненте, когда это нужно. Вы создаете композиционную функцию, которая отслеживает координаты x и y, и затем возвращает их клиентскому коду.

import { ref, onMounted, onUnmounted } from "vue";

export function useMousePosition() {
  const x = ref(0);
  const y = ref(0);

  function update(e) {
    x.value = e.pageX;
    y.value = e.pageY;
  }

  onMounted(() => {
    window.addEventListener("mousemove", update);
  });

  onUnmounted(() => {
    window.removeEventListener("mousemove", update);
  });

  return { x, y };
}

Если вы хотите использовать эту логику в компоненте и вызываете эту функцию, то деструктурируйте возвращаемый объект и затем возвращайте x и y координаты в шаблон.



Это будет работать, но если, поглядев на функцию, вы решили провести рефакторинг и возвращать объект position вместо x и y:

import { ref, onMounted, onUnmounted } from "vue";

export function useMousePosition() {
  const pos = {
    x: 0,
    y: 0
  };

  function update(e) {
    pos.x = e.pageX;
    pos.y = e.pageY;
  }

  // ...
}

Проблема этого подхода в том, что клиент композиционной функции должен иметь ссылку на возвращаемый объект всегда, чтобы реактивность сохранялась. Это означает, что мы не можем деструктурировать объект или применить spread оператор:

// компоте клиент
export default {
  setup() {
    // потеря реактивности!
    const { x, y } = useMousePosition();
    return {
      x,
      y
    };

    // потеря реактивности!
    return {
      ...useMousePosition()
    };

    // это единственный способ сохранить реактивность
    // надо возвращать `pos` как есть, и обращаться в шаблоне к x и y так: `pos.x` и `pos.y`
    return {
      pos: useMousePosition()
    };
  }
};

Но это не означает, что мы не можем в таком случае использовать reactive. Есть функция toRefs (), которая сконвертирует реактивный объект в простой объект, каждое свойство которого это ref соответствующего свойства оригинального объекта.

function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0
  });

  // ...
  return toRefs(pos);
}

// x & y теперь ref!
const { x, y } = useMousePosition();

Таким образом надо учитывать некоторые аспекты при создании композиционных функций. Если вы понимаете, как клиентский код работает с вашими функциями, то все будет ОК.

ИТОГ


Когда я впервые начал использовать Composition API, я был озадачен вопросом, когда применять ref (), а когда reactive (). Возможно я до сих пор делаю это не правильно, и пока кто-нибудь не скажет мне этого, я буду придерживаться этого подхода. Надеюсь помог прояснить некоторые вопросы, и хотелось бы услышать ваше мнение.

Happy Coding

© Habrahabr.ru