Понимание реактивности во Vue.js (Шаг за Шагом)

Spread the love

В этой статье я собираюсь рассказать о реактивности во Vue. Начнем с определения, что такое реактивность.

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

Если это попытаться объяснить в математическом определении, то у нас будет уравнение, в котором y = x + 1, x – наша независимая переменная, а y – наша зависимая переменная. Почему у называется зависимой переменной? Ну, потому что его значение зависит от значения х!

Хорошо известным примером системы реактивности является электронная таблица.

Шаг 1

Давайте забудем о фреймворках и шаг за шагом реализуем их в vanilla javascript. Для начала давайте сохраним нашу переменную x в объекте состояния (позже я объясню, почему она находится внутри объекта). Затем создадим функцию для отображения значения y в html. Наконец, вручную запустим функцию:

<h1 id="y"></h1>

let state = { 
  x: 1
};

function renderY() {
  document.getElementById("y").innerText = `y = x + 1 = ${state.x + 1}`;
}
renderY();

Демо:

Шаг 2

Теперь давай те сделаем так что бы вместо ручного вызова функции renderY, у нас бы была бы функция setState, которая сама обновляла бы наше состояние и запускала бы функцию render.

function setState(newState) {
 state = { 
   ...state, 
   ...newState 
 };
 renderY();
}

В итоге мы получили React! 🙂

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

Ответ – да!

Object.defineProperty(obj, key, {
 get() {
   //...
 },
 set(newValue) {
   // ...
 }
});

Метод defineProperty для Object является функцией только для es5+. Вот почему vue 2 не поддерживает IE8 и ниже.

Это API позволяет нам переопределить поведение некоторых объектов по умолчанию, например, геттеров (getter) и сеттеров (setter).

Также именно поэтому мы хотим, чтобы состояние (state) было объектом. И это также объясняет, почему у vue есть некоторые махинации с массивами. Потому что это API отсутствует в массивах.

Давайте вызовем нашу функцию рендеринга в сеттере!

let value = state["x"];

Object.defineProperty(state, "x", {
 get() {
   return value;
 },
 set(newValue) {
   value = newValue;
   renderY();
 }
});

Мы берем значение x в начале, затем в геттере просто возвращаем его значение и в сеттере запускаем нашу функцию рендеринга после обновления значения x.

Демо:

Это работает, но наш код выглядит так ужасно. Он работает только для свойства “x” и не работает для вложенного объекта. Давайте проведем рефакторинг нашего кода.

Шаг 3

Давайте создадим наблюдаемую функцию. Эта функция будет просто перебирать все ключи объекта и рекурсивно делает все вложенные объекты наблюдаемыми.

function observable(obj) {
 Object.keys(obj).forEach(key => {
   let value = observable(obj[key]);

   Object.defineProperty(obj, key, {
     get() {
       return value;
     },
     set(newValue) {
       value = newValue;
       renderY();
     }
   });
 });
 return obj;
}

Демо:

Из демонстрации, которую вы можете увидеть в этом особом случае, наш код все еще работает. Тем не менее, при обновленном состоянии наш код может выполнять только одну отдельную работу – рендеринг Y, и он всегда будет рендерить Y, независимо от того, какое состояние обновлено. Я считаю, что это ошибка. Поэтому мы должны найти способ заставить наше наблюдаемое состояние отслеживать, какой код на самом деле зависит от того, от какого состояния.

Шаг 4

Чтобы отслеживать зависимости кода, давайте создадим простой класс Dep, чтобы мы могли использовать его для создания экземпляра dep для каждого ключа объекта.

class Dep {
 static job;

 constructor() {
   this.jobs = new Set();
 }

 depend() {
   if (Dep.job) {
     this.jobs.add(Dep.job);
   }
 }

 notify() {
   this.jobs.forEach(job => {
     job();
   });
 }

}

Dep.job = null;

Как вы можете видеть, этот класс довольно прост: в конструкторе мы просто создаем набор заданий jobs, причина использования Set в том, что мы не хотим добавлять дубликаты заданий для одного и того же ключа.

Метод depend просто добавляет текущее задание job в набор заданий jobs а метод notify запускает все добавленные задания.

Обратите внимание, что у нас также есть статическая переменная job для хранения текущего оцениваемого задания.

Теперь давайте используем класс Dep для обновления нашей наблюдаемой функции.

function observable(obj) {
 Object.keys(obj).forEach(key => {
   let value = observable(obj[key]);
   const dep = new Dep();

   Object.defineProperty(obj, key, {
     get() {
       dep.depend();
       return value;
     },
     set(newValue) {
       value = newValue;
       dep.notify();
     }
   });
 });
 return obj;
}

Для каждого ключа мы создаем новый экземпляр класса Dep для отслеживания зависимой job.

Но как мы узнаем, зависит ли текущее задание от этого свойства объекта? Правильно, нам просто нужно запустить это задание один раз (что мы уже делали в предыдущих демонстрациях), когда задание запускается, если задание пытается получить доступ к этому свойству объекта, мы знаем, что это задание зависит от этого свойства. Поэтому мы можем вызвать dep.depend(), чтобы добавить текущее задание к экземпляру Dep внутри геттера.

И затем, когда это свойство обновляется, мы хотим перезапустить все сохраненные задания, вызвав dep.notify() внутри сеттера.

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

Таким образом, как вы можете видеть, при запуске задания renderY функция пытается получить state.x, поэтому будет запущен метод геттер для x, поэтому задание renderY будет добавлено в job, установленные для x.

Шаг 5

В нашем демо у нас есть только одна работа, renderY. Теперь я хочу получить другую job для renderX, но я не хочу копировать и вставлять код для установки текущей job; потом снова запусткать job, а затем очищать job.

Опять же, как хороший разработчик, мы всегда должны помнить о принципе DRY.

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

function runner(job) {
 Dep.job = job;
 job();
 Dep.job = null;
}

Давайте вернемся к нашему codepen и добавим больше раннеров;

К настоящему времени мы только что внедрили очень базовую версию системы реактивности vue. Конечно, настоящий код vue гораздо сложнее, чем этот. Потому что нужно рассмотреть множество исключений и крайних случаев, а код должен быть более организованным.

Но если вы захотите проверить исходный код vue, вы увидите класс Dep, функцию, которая делает объект реактивным, и класс наблюдателя (watcher), выполняющий в основном ту же работу, что и наша простая функция runner.

В целом, эта система, основанная на API object.defineProperty, работает нормально в большинстве случаев. Но каковы особенности этой системы? Помните, я говорил, что API object.defineProperty не работает с массивом? Vue сделал переопределение некоторых методов-прототипов, таких как array.push, чтобы сделать массив реактивным. Другое дело, что таким образом мы только активируем начальные свойства объекта, а как насчет новых свойств, которые вы добавите к объекту позже? Vue не может знать о новом свойстве заранее, поэтому оно не может сделать его реактивным. Поэтому каждый раз, когда вы хотите добавить новое реактивное свойство к объекту данных, вы должны использовать Vue.set.

Тем не менее, всех этих махинаций больше не будут в Vue 3.0. Потому что система реактивности Vue 3.0 будет полностью переписана с помощью Object Proxy. В настоящее время Vue 3.0 все еще находится в активной стадии разработки, и мы пока не не можем увидить исходный код. Однако Evan You, создатель Vue, объяснил, как это будет работать на семинаре, который я посетил на конференции Vue в Торонто несколько месяцев назад. Сейчас я покажу вам, ребята, что я узнал из этого.

Еще кое-что…

Proxy – это новый глобальный API, представленный в ES6. Это функция уровня языка, которая означает, что это не полифиллинг, а также означает, что Vue 3.0 по умолчанию не поддерживает IE11 и ниже. Однако вы можете настроить свою сборку для использования старой системы реактивности на основе object.defineProperty.

Если вы ранее не слышали о Proxy API, вы можете прочитать какой-нибудь документ об этом на одном MDN или в вашем любимом w3school.

По сути, API принимает ваш исходный объект в качестве первого параметра, а вторым параметром будет объект обработчиков (handlers), или как их еще называют trap-сообщениями, в том числе set trap, get trap, delete trap и т. д. Затем API просто возвращает прокси-объект , Обратите внимание, что возвращаемое значение и исходный объект – это два разных объекта. Потому что Proxy API действительно не изменяет ваш объект напрямую, как это делает объект defineProperty. Это также означает, что мы не можем хранить объекты внутри самого объекта. Но не волнуйтесь, у нас будет решение этого.

const depsStorage = new WeakMap()

const handlers = {
 get(target, key, receiver) {
   let deps = depsStorage.get(target)
   if (!deps) {
     deps = {}
     depsStorage.set(target, deps)
   }
   let dep = deps[key]
   if (!dep) {
     dep = deps[key] = new Dep()
   }
   dep.depend()
   return observable(target[key])
 },
 set(target, key, value) {
   target[key] = value
   let deps = depsStorage.get(target)
   if (!deps) {
     return
   }
   const dep = deps[key]
   if (dep) {
     dep.notify()
   }
 }
}

Прежде всего, давайте определим наши traps. В геттере trap мы создаем deps, если он еще не создан в хранилище, далее мы вызываем dep.depend(), чтобы зарегистрировать текущее задание как зависимость, как мы это делали раньше. То же самое в сеттере trap, мы обновляем значение в target, а затем вызываем dep.notify() для запуска job.

const observedValues = new WeakMap()
function observable(obj) {
 if (!obj || typeof obj !== 'object') {
   return obj
 }
 if (observedValues.has(obj)) {
   return observedValues.get(obj)
 }
 const observed = new Proxy(obj, handlers)
 observedValues.set(obj, observed)
 return observed
}

Затем мы обновляем нашу наблюдаемую функцию, чтобы использовать Proxy с нашими обработчиками. Мы создаем WeakMap для хранения наблюдаемого объекта, поэтому мы не будем наблюдать один и тот же объект несколько раз.

Демо:

Заключение

В этой статье я рассказал как устроена реактивность в Vue 2 и как будет устроена в будущей версии Vue 3. Если у вас есть вопросы или свое мнение на этот счет добро пожаловать, в комментарий автора статьи.

Оригинальная статья An Xie: Understand Vue Reactivity Implementation Step by Step

Была ли вам полезна эта статья?
[4 / 5]

Spread the love
Подписаться
Уведомление о
guest
1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
alev
alev
3 лет назад

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

наверно имелись ввиду все использующие его значения, а не используемые.