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

Скорее всего, вы не будете использовать ванильный JS для создания своих сложных приложений. Вместо этого вы можете использовать фреймворк по вашему выбору: Angular, React, Vue и т. Д. Способ, которым эти фреймворки обрабатывают изменения, совершенно разный, но их конечная цель одна и та же: минимизировать количество манипуляций с DOM, насколько это возможно, поскольку они довольно дорого.

Что вызывает изменение состояния в JavaScript?

Все, что является асинхронным по своей природе, вызовет изменение состояния. Они могут быть любыми из следующих:

  1. События (взаимодействия с пользователем, настраиваемые события)
  2. setTimeout, setIntervals
  3. Сетевые запросы

Давайте посмотрим на механизмы обнаружения изменений, используемые некоторыми популярными фреймворками JS.

Angular использует замечательную библиотеку под названием zone.js, которая отслеживает все асинхронные вызовы и, таким образом, сообщает Angular, когда что-то меняется. Затем Angular запускает обнаружение изменений для этого компонента и всех его дочерних элементов.

React, с другой стороны, полагается на метод setState, чтобы знать, когда что-то меняется (он все еще не знает, что именно изменилось), и создает новую виртуальную DOM с этими изменениями. Затем он эффективно сравнивает этот vDOM с предыдущим, чтобы получить различие, и, наконец, применяет это различие к реальному DOM.

Vue.js следует механизму, аналогичному тому, что есть в Knockout, Ember, Meteor, для обнаружения изменений на уровне свойств, и, кроме того, использует Virtual DOM (начиная с версии 2 и далее) для повышения производительности.

Механизм, используемый Vue, - это система отслеживания зависимостей, которая стала возможной благодаря этой концепции: Перехват. С появлением ES5 в JS были введены геттеры и сеттеры. Это дает возможность контролировать (перехватывать) при доступе к свойству или его изменении.

Рассмотрим пример: существует переменная с именем «a». Вы можете думать об этом как о свойстве «данные» в компоненте Vue. Возможно, вы захотите, чтобы в вашем шаблоне отображалась буква «а». Вы, вероятно, сделали бы что-то вроде этого: ‹p› {{a}} ‹/p›. На этом этапе свойство данных «a» «привязано» к шаблону с помощью интерполяции. Теперь нам нужно знать, когда "a" меняется, и соответственно обновлять его шаблон. Другими словами, выражение привязки теперь зависит от «a». И может быть еще много свойств / выражений, которые тоже зависят от «а». Итак, теперь нам по сути нужен список зависимостей от «a», и таким образом всякий раз, когда «a» изменяется, он может уведомить всех своих зависимых. В Vue эти зависимости представляют собой не что иное, как выражения шаблона привязки и вычисляемые свойства. Вычисляемые свойства зависят от других данных / вычисленных свойств для их существования (например, b = a * 10).

Возвращаясь к нашему простому примеру ‹p› {{a}} ‹/p›, когда мы впервые видим эту привязку, мы собираемся преобразовать ее в функцию рендеринга. Причина, по которой мы делаем это, заключается в том, что эту функцию можно установить как функцию «обратного вызова», которую можно вызвать позже, когда мы захотим повторно визуализировать шаблон.

Регистрации зависимостей

Когда свойство / выражение зависит от другого свойства, ему необходимо выполнить 3 шага, чтобы отслеживать изменения:

  1. Устанавливает функцию обратного вызова для глобального активного целевого свойства. Причина, по которой это будет глобальное свойство, заключается в том, что JavaScript является однопоточным и в любой момент времени действительно будет только один объект, который может получить доступ к этой цели
  2. Доступ к свойству, от которого он зависит. В случае выражения привязки он будет запускать функцию рендеринга, а в случае вычисленных свойств он будет вызывать свою вычисляемую функцию, обе из которых будут вызывать свои зависимые свойства
  3. Сбросить глобальную активную цель на null, чтобы другие свойства могли использовать это для регистрации в качестве зависимостей

Реактивные свойства

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

  1. В получателе свойства мы собираемся проверить, есть ли какая-либо глобальная активная цель. Если это так, мы добавляем значение этой цели (которое, по сути, является функцией обратного вызова fn) в список зависимостей свойства с помощью функции подписки.
  2. В установщике свойства мы собираемся вызвать функцию уведомления, которая, в свою очередь, вызовет функции обратного вызова для всех своих зависимостей

Диаграмма зависимости

Вот простой пример диаграммы зависимостей с 1 свойством данных, 3 выражениями шаблона привязки и 2 вычисляемыми свойствами:

Из этого графика можно сделать вывод о небольшой разнице между вычисляемым свойством и выражением привязки. Допустим, для свойства A стрелка за пределами от A до B означает, что A зависит от B, а in-bound от C до A означает, что C зависит от A. . Как минимум всегда будет одна ограничивающая стрелка и одна входящая стрелка для вычисляемого свойства, тогда как для шаблона привязки никогда не будет входящей стрелки. Другими словами. , вычисляемое свойство будет поддерживать свой собственный список свойств, которые зависят от него, помимо того, что оно зависит от других свойств. Они также отличаются от свойств data в том смысле, что свойства data не зависят от чего-либо еще (0 стрелки для исходящего сообщения).

Код…

Хватит говорить. Давайте попробуем закодировать эти концепции.

Мы собираемся создать класс Dependency Tracker, который будет иметь активную цель в качестве глобального свойства. Кроме того, у него будет 2 функции: подписаться и уведомить. Функция подписки добавит обратный вызов «активная цель» в свой список подписчиков. Функция уведомления будет функцией обратного вызова для каждого подписчика.

PS: Я добавил свойство name для ведения журнала

Теперь, когда у нас есть механизм отслеживания зависимостей, мы собираемся использовать его в наших свойствах, чтобы сделать их «реактивными». Сначала мы рассмотрим свойства «данных» компонента Vue, поскольку их легче всего объяснить.

Функция initData в приведенном ниже коде выполняет следующие действия: Просматривает каждое свойство data в компоненте (включая вложенные свойства) и для каждого из них:

  1. создать объект отслеживания зависимостей
  2. инициализировать "геттер" и "сеттер" для каждого из них. Получатель проверяет наличие «активной» цели и, если она существует, добавляет ее в свой список иждивенцев. Установщик назначит новое значение (если новое значение является объектом, он рекурсивно повторно инициализирует значение), а затем уведомит всех своих зависимых

Мы рассмотрели добавление зависимостей и их уведомление в свойствах данных с помощью объекта отслеживания зависимостей. В нашем коде все еще есть 2 недостающих фрагмента:

Как собственность, которая зависит от другой собственности, регистрируется сама?

Кто будет выполнять функцию обратного вызова для повторного рендеринга шаблона / повторного вычисления вычисленной fn?

Поскольку нас не интересует часть привязки шаблона (v-bind или усы), я использовал дополнительное свойство в компоненте, которое называется 'templates ', чтобы узнать, какое свойство привязать к какому шаблону:

HTML
<div id="a"></div>
JS
let templateForA = function() {
  return `<p>Hello, I am a <i>data</i> prop. You can call me <strong>A</strong>: ${this.a}</p>`
}
templates: [{
      html: templateForA,
      prop: "a"
    },

Для 1 мы собираемся выполнить 3 шага, которые мы обсуждали в разделе Регистрация зависимостей. Функция bindPropToTemplate делает именно это в строках 7, 8 и 9. Давайте пройдемся строка за строкой:

Строка 7: Назначьте активной цели «обратный вызов», о котором мы говорили ранее. Этот обратный вызов здесь сохраняется в объекте Watcher. Мы обсудим этот объект подробно в ближайшее время.

Строка 8: Запустите функцию рендеринга, которая вызывает все необходимые права доступа к свойствам

Строка 9: Сбросьте значение цели до нуля, чтобы ее могли использовать другие свойства

Наблюдатель: ответ на наш второй недостающий элемент! Он делает следующее:

сохраняет обратный вызов и превращает его в функцию под названием «update».

Функция обновления будет запускаться функцией «уведомить» (в функции объекта отслеживания зависимостей) для всех ее подписчиков.

Примечание: Фактическая реализация Watcher во Vue намного сложнее. Он отвечает за сбор и очистку зависимостей. Он также отвечает за объект $ watch.

Для простоты, вот минимальная функция, которую мы будем использовать:

Вот наша простая функция рендеринга, которая применяет шаблон к DOM:

Примечание: addAndRemoveClass используется в демонстрации для выделения изменений в DOM.

А как насчет вычисляемых свойств?

Когда мы обсуждали диаграмму зависимостей, мы пришли к выводу, что вычисляемые свойства должны быть уведомлены при изменении их зависимостей, а они, в свою очередь, должны уведомлять своих иждивенцев. По этой причине каждое вычисляемое свойство будет иметь свой собственный объект «Наблюдатель». Обратный вызов для наблюдателя будет делать 2 вещи:

  1. Пересчитайте функцию и кешируйте последний результат (строка № 10). Мы кэшируем его, чтобы не пересчитывать функцию, если ее зависимости не меняются.
  2. Уведомить своих иждивенцев (строка № 11)

Строки 14, 15 и 16 аналогичны 3 шагам, которые мы видели в функции bindingPropToTemplate:

  1. Назначьте Наблюдатель активной цели

2. Запустите вычисленную функцию (это только в первый раз) и назначьте ее кеш-памяти

3. Сбросьте активную цель на «null»

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

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

Что касается «свойств», он будет вести себя аналогично свойству данных, но без «установщика», поскольку они просто наследуют свои значения от своих родительских компонентов.

А как насчет массивов и динамических добавлений свойств?

Когда дело доходит до обнаружения изменений, массивы немного сложны. Мы можем исправлять его собственные методы, такие как «push, splice, pop, unshift, shift, reverse», и уведомлять его зависимые элементы при изменении массива с помощью этих методов.

Однако невозможно обнаружить изменения, когда к массиву обращаются по его индексу или когда вы изменяете длину массива напрямую (как обсуждается здесь: https://vuejs.org/v2/guide/list.html#Caveats)

Вот почему Vue просит вас использовать метод «splice» или метод Vue.set, чтобы он мог уведомлять зависимые элементы массива о его изменении.

Это по той же причине, что вы не можете динамически добавлять новые свойства к объекту, поскольку они еще не являются реактивными. Вы можете использовать метод Vue.set или объявить все свойства во время инициализации (как обсуждается здесь: https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats)

С появлением ES2015 Proxies в JavaScript стало намного проще перехватывать свойства на уровне объекта (особенно манипулировать массивами), и Vue будет использовать их в v3!

Обратите внимание, что исходный код демонстрации не распространяется на массивы, а это просто очень простая версия реактивной системы.

Демо: https://reactive-framework.firebaseapp.com/

Исходный код демонстрации: https://github.com/abhi7cr/reactive

Ссылка на презентацию (с недавней встречи в Чикаго, посвященной Vue.js): https://gitpitch.com/abhi7cr/reactive#/

Ссылки:

Если вы хотите углубиться в то, как реактивность решается в исходном коде vue, вы можете найти ее здесь: