Реактивность описывает ситуацию, при которой изменения в состоянии приложения автоматически отображаются в DOM.

Vue - это прогрессивный фреймворк для создания современных реактивных пользовательских интерфейсов и отдельных приложений. Vue обеспечивает расширяемый рендеринг HTML-разметки с помощью объявления шаблонов с привязанными данными.

Vue.js имеет адаптируемую реактивную архитектуру, которая фокусируется на декларативном рендеринге данных и набора компонентов.

Реактивность во Vue позволяет фреймворку взаимодействовать на каждом уровне приложения и обновляет модификацию данных представления.

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

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

<template>
  <h1>Меня зовут {{ name }}</h1>
</template>
<script>
  export default {
    data() {
      return {
        name: "Иван"
      };
    }
  };
</script>

Во второй версии Vue отслеживает все свойства и использует Object.defineProperty() для создания геттеров и сеттеров для каждой части данных и обеспечения отслеживания изменений данных.

Согласно веб-документации, object.defineProperty() определяет новое свойство непосредственно для объекта. Метод также может изменять существующее свойство объекта и возвращать объект. С помощью object.defineProperty мы можем легко установить геттеры и сеттеры объекта:

const data = {
  count: 10
};
const newData = {};

Object.defineProperty(newData, 'count', {
  get() { return data.count; },
  set(newValue) { data.count = newValue; },
});

console.log(newData.count);  // => 10

newData.count = 20;
console.log(newData.count);  // => 20

Мы создали два объекта: data и newData (пустой). data - наш исходный объект, а newData будет служебным объектом. Устанавливаем свойство count с помощью object.defineProperty().

Чтобы отслеживать, когда к свойству обращаются или когда его изменяют, мы можем сделать следующее:

const data = {
  count: 10
};
const newData = {};

function track(){
  console.log('К свойству обратились')
};
function trigger(){
  console.log('Свойство изменили')
};

Object.defineProperty(newData, 'count', {
  get() { track(); return data.count; },
  set(newValue) { data.count = newValue; trigger(); },
});

console.log(newData.count);  // К свойству обратились 10

newData.count = 20;  // Свойство изменили

console.log(newData.count);  // К свойству обратились 20

С помощью функций trigger и track мы можем легко увидеть, когда происходят изменения, и выполнить операции отслеживания зависимостей и уведомления об изменениях.

Как разработчику на Vue, важно, чтобы у вас действительно было твердое представление о том, как работает реактивность во Vue 3, чтобы избежать различного рода ее ограничений - особенно во Vue 2. Вам также необходимо понимать реактивность, чтобы использовать такие новые функции, как API композиции.

По-умолчанию JavaScript не реактивен:

let speed = 5;
let time =  2;
let length = speed * time;
console.log(`Пройденное расстояние - ${total}`);  // => Пройденное расстояние - 10
time = 3;
console.log(`Пройденное расстояние - ${total}`);  // => Пройденное расстояние - 10

Как видим, значение length не было обновлено после того, как мы изменили time на 3.

Чтобы добавить немного реактивности нашему коду, мы можем превратить вычисление в функцию:

let speed = 5;
let time =  2;
let length = 0;

// объявим вычисление
const computeLength = () => length = speed * time;
computeLength();

console.log(length);  // => 10
time = 3;
console.log(length);  // => по-прежнему 10

computeLength();  // запускаем опять вычисление
console.log(length);  // => 15

Теперь мы можем запустить функцию вычисления, чтобы обновить вычисленное значение.

В приведенном выше примере кода мы вручную выполнили вычисление после изменения значения time, чтобы получить обновленное значение length. Однако такой процесс ненадежен, и вычисления приходится выполнять вручную, что может быть проблемой - это может отрицательно сказаться на производительности.

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

Во Vue 3 отвечающая за реактивность API была переписана с целью устранения некоторых недостатков Vue 2. Реактивная система была переписана для использования прокси JavaScript. Прокси-сервер действует как оболочка вокруг объекта или функции, которая перехватывает операции получения значения и установки/изменения их.

С новой реактивной системой во Vue 3 теперь улучшена поддержка наблюдения за изменениями данных с помощью прокси-объектов.

Объекты Proxy позволяют вам создавать прокси для другого объекта, который может перехватывать и переопределять основные операции для него.

С помощью прокси мы можем перехватывать такие операции, как get и set, и сразу же видеть, когда данные доступны или изменены.

Чтобы создать прокси, вам нужно передать два параметра:

  • target: объект данных
  • handler: объект, который определяет операции, которые вы хотите перехватить.

Давайте рассмотрим базовый пример ниже:

const data = {
  name: 'Иван'
};
const handler = {
  get(target, prop, receiver){
    console.log('Получение данных: ', target, prop);
    return target[prop];
  },
  set(target, key, value, receiver) {
    console.log('Установка данных: ', target, key, value);
    return target[key] = value;
  }
};
const proxy = new Proxy(data, handler);

console.log(proxy);  // => { name: 'Иван' }
console.log(proxy.name);
// => Получение данных: { name: 'Иван' } name
// => Иван

console.log(proxy.name = 'Вася');
// => Установка данных: { name: 'Иван' } name Вася
// => Вася

В приведенном выше примере мы создали прокси-объект, который принял объект данных. У нас также есть обработчик handler, который мы используем для перехвата операций get и set в наших объектах. Каждый раз, когда мы пытаемся получить доступ к параметру name, консоль отображает Получение данных: { name: 'Иван' } name. Подобное происходит, когда мы пытаемся обновить значение свойства name в проксируемых объектах.

Чтобы поддерживать нормальное поведение геттеров и сеттеров объектов, нам нужно использовать объект Reflect для отражения нормального поведения объекта.

Взглянем на пример ниже:

const data = {
  name: 'Иван'
};
const handler = {
  get(target, prop, receiver){
    console.log('Получение данных: ', target, prop);
    return Reflect.get(target, prop, receiver);
  },
  set(target, key, value, receiver) {
    console.log('Установка данных: ', target, key, value);
    return Reflect.set(target, key, value, receiver);
  }
};
const proxy = new Proxy(data, handler);

console.log(proxy);  // => { name: 'Иван' }
console.log(proxy.name);
// => Получение данных: { name: 'Иван' } name
// => Иван

console.log(proxy.name = 'Вася');
// => Установка данных: { name: 'Иван' } name Вася
// => Вася

С Reflect нам не нужно обрабатывать мануально прописанные параметры, как мы это делали раньше.

Чтобы увидеть, когда данные поступили или изменились, мы можем добавить три функции:

  • track: сообщает нам, когда получен доступ к данным
  • watch: информирует нас, когда устанавливаются параметры объекта
  • trigger: информирует нас об изменении данных в объекте

Теперь к примеру:

const data = {
  name: 'Иван',
  surname: 'Иванов',
};
const track = (target, prop, receiver) => console.log('Получение данных: ', target, prop);
const trigger = (target, key, value, receiver) => console.log('Изменение данных: ', target, key, target[key], value);
const watch = (target, key, value, receiver) => console.log('Установка данных: ', target, key, target[key], value);
const handler = {
  get(target, prop, receiver){
    track(target, prop, receiver);
    return Reflect.get(target, prop, receiver);
  },
  set(target, key, value, receiver) {
    watch(target, key, value, receiver);
    if (target[key] != value) {
      trigger(target, key, value, receiver);
    };
    return Reflect.set(target, key, value, receiver);
  }
};
const proxy = new Proxy(data, handler);

console.log(proxy);  // => { name: 'Иван', surname: 'Иванов' }
console.log(proxy.name);
// => Получение данных: { name: 'Иван', surname: 'Иванов' } name
// => Иван

console.log(proxy.name = 'Вася');
// => Установка данных: { name: 'Иван', surname: 'Иванов' } name Иван Вася
// => Изменение данных: { name: 'Иван', surname: 'Иванов' } name Иван Вася
// => Вася

console.log(proxy.name = 'Петя');
// => Установка данных: { name: 'Вася', surname: 'Иванов' } name Вася Петя
// => Изменение данных: { name: 'Вася', surname: 'Иванов' } name Вася Петя
// => Петя

console.log(proxy.name = 'Петя');
// => Установка данных: { name: 'Петя', surname: 'Иванов' } name Петя Петя
// => Петя

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

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

Происходит преобразование данных в прокси-объект, которое позволяет Vue выполнять отслеживание зависимостей и уведомлять об изменении при доступе к свойствам или их изменении.

Объединив все, что мы знаем сейчас, мы сможем создать наш собственный реактивный метод:

const reactive = (data) => {
  const track = (target, prop, receiver) => console.log('Получение данных: ', target, prop);
  const trigger = (target, key, value, receiver) => console.log('Изменение данных: ', target, key, value, receiver);
  const watch = (target, key, value, receiver) => console.log('Установка данных: ', target, key, value, receiver);
  const handler = {
    get(target, prop, receiver) {
      track(target, prop, receiver);
      return Reflect.get(target, prop, receiver);
    },
    set(target, key, value, receiver) {
      watch(target, key, value, receiver);
      if (target[key] != value ) {
        trigger(target, key, value, receiver);
      };
      return Reflect.set(target, key, value, receiver);
    }
  };
  const proxy = new Proxy(data, handler);
  return proxy
};

const store = reactive({
  count: 0
});

store.count;
// => Получение данных: { count: 0 } count
// => 0

console.log(++store.count);
// => Получение данных: { count: 0 } count
// => Установка данных: { count: 0 } count 1 { count: 0 }
// => 1

Мы только что создали реактивный метод, который превращает наш объект данных в прокси. Это позволяет уведомлять нас об изменениях при каждом изменении свойства.

Эти прокси-объекты не могут быть видны пользователю, но внутри они позволяют Vue выполнять отслеживание зависимостей и уведомлять об изменении при доступе к свойствам или их изменении. Команда Vue выпустила пакет реактивности как отдельный пакет во Vue 3.

Vue отслеживает все объекты, которые были сделаны реактивными, поэтому всегда возвращает один и тот же прокси для одного и того же объекта.

Таким образом, мы можем сказать, что у каждого экземпляра компонента есть соответствующий экземпляр наблюдателя, который записывает все свойства, "затронутые' во время рендеринга компонента, как зависимости. Если в будущем установщик зависимости запускается, он уведомляет наблюдателя, который, в свою очередь, вызывает повторную отрисовку компонента.

При первой отрисовке компонент будет отслеживать список зависимостей (или данных в объектах). Это свойства, к которым он получил доступ во время рендеринга. Это делает компонент подписчиком каждого из этих свойств. В результате, когда прокси-сервер перехватывает операцию, свойство уведомляет все свои подписанные компоненты о повторном рендеринге.