Давайте поговорим о том, что внутри, под капотом Alpine.js. К концу статьи вы узнаете, как происходит создание современного реактивного фронтенд фреймворка путем написания оного своими руками. Это легче, чем вам кажется. Предыдущие статьи серии: Раньше фронтенд-фреймворки больше напоминали мне черный ящик, у которого неизвестно, что внутри. Но после написания своего я осознал, что на самом деле основные компоненты фреймворка не так уж и сложно понять. Чтобы вас в этом убедить, предлагаю вместе написать Alpine.js с нуля. Сначала создадим базовый компонент Alpine.js. Максимально простой. Какая функциональность нам нужна? Любой фронтенд-фреймворк работает по следующему алгоритму: При нажатии на определенную кнопку, значение в <span> должно меняться. "Умная система" должна проходится по DOM и обновлять то, что должно быть обновлено. В данном примере, когда значение count меняется, нужно, чтобы поменялось всё, что на этот count ссылается. Первое, что нам нужно сделать, – получить данные. Если мы посмотрим в консоли, чему равна переменная dataString, мы получим строку "{ count: 0 }". Как преобразовать её в объект? Первое, что приходит в голову – JSON.parse(). Но нам это не подойдет, потому что JSON.parse() очень строгий. Он требует JSON-нотации, где все ключи будут в двойных кавычках, как '{ "count": 0 }'. Это не очень гибко. Что нам отлично подойдет – это eval(). Во фронтенд-фреймворке он не так опасен, как, например, в PHP, но если вы погуглите, то вам довольно быстро объяснят, почему пользоваться им не надо. Сейчас Alpine.js уже не использует eval(), но для упрощения в нашем примере мы им воспользуемся. Для того, чтобы не конкатенировать строки, мы используем обратные кавычки. А чтобы выражение внутри eval() возвращало нам объект, мы оборачиваем все в круглые скобки. Выведем получение данных в отдельную функцию: Первая цель выполнена – мы получили данные из нашего компонента. Второе, что нужно сделать, – вывести count в <span>. То есть пройтись по DOM-дереву и обновить в нем необходимые значения. Есть несколько способов, как это сделать. Нам нужен цикл, который проходится по элементам и проверяет, есть ли у них атрибут x-text. Если есть, то он прочитывает значение атрибута, находит это значение в данных и обновляет на него innerText элемента. Мы можем сделать так: Но, во-первых, это не будет работать с модификаторами, например, @click.window. Во-вторых, это медленно. Давайте сделаем то же самое, что делает оригинальный Alpine.js – он "шагает" по DOM. Начиная с родительского элемента с x-data, он рекурсивно проходится по всем элементам. Если у элемента есть потомки, он проходится и по ним. Сделаем функцию walkDom(). Начнем с того, как она используется: Первый аргумент – элемент, второй – колбэк. Для начала мы просто выведем каждый элемент в консоль. А вот сама функция walkDom(). Она, на самом деле, очень простая, но, как и любая рекурсия, немного сложна для восприятия. Сначала вызываем колбэк (1). Получаем первого потомка (2). Если у элемента есть потомок (3), то вызываем walkDom() еще раз (4), с этим потомком. Который, если имеет потомка (2), вызывает walkDom() со своим потомком (4). И так далее, пока не закончатся все потомки. Затем переходим к следующему потомку предыдущего элемента (5). И делаем то же самое. Функция остановится, когда запустится для каждого элемента, независимо от того, как глубоко он запрятян. Можно попробовать. Для наглядности добавим еще один div с вложенным внутрь span В консоли мы получим следующее:  Сначала мы получаем внешний элемент со всеми потомками. Потом первый button, затем второй button, span, div и внутренний span. Отлично! Теперь мы умеем "ходить" по DOM. Всё, что осталось, – изменить наш колбэк. Мы проверяем, есть ли у элемента атрибут x-text, и, если есть, меняем его innerText на значение из объекта. В нашем случае – count. Посмотрим на результат:  count is not defined. Ничего удивительного. Мы делаем innerText элемента равным count, но никакого count в глобальной видимости (window) у нас нет. Для того, чтобы это работало правильно в HTML мы должны писать <span x-text="data.count"></span>. Теперь всё работает, как надо. Но нам бы не хотелось писать каждый раз data.*, это неудобно. Мы хотим писать count, но подразумевать data.count. Как это сделать? Когда я столкнулся с этой проблемой, я застрял на несколько дней, пока не догадался посмотреть в исходники Vue.js, и не узнал, что делают они. В JavaScript, когда мы пишем variable, мы подразумеваем window.variable. Движок, когда ищет глобальную переменную, сам смотрит в объект window и, если не находит её там, выбрасывает ошибку. Всё, что нам нужно сделать, – это сказать движку, что перед тем, как смотреть в window, он должен посмотреть в нашу переменную data. Как это сделать? На самом деле, очень просто. Этот функционал встроен в язык с момента его создания. Это выражение with() {}. В "обычном" программировании его рекомендуется не использовать, так как он создает путаницу. Но в нашем случае – эта именно та часть пазла, которой нам так не хватает. Готово! x-text заработал. Теперь, если написать в консоли в браузере мы увидим 4. Что дальше? Думаю, вы уже догадались. Было бы неплохо, чтобы на каждое изменение DOM обновлялся сам. И это будет уже настоящая реактивность, к которой мы так стремимся. Нам нужно вызывать refreshDom() каждый раз, когда меняются данные. То есть, нам нужно наблюдать за изменениями данных. Как это сделать? В JavaScript наблюдение за изменениями было довольно сложным до добавления в ES6 Proxy. Прокси оборачивает объект и следит за всеми его изменениями. Поначалу они казались мне сложными, но, на самом деле, они очень простые. Прокси – это класс, который принимает два параметра – объект, за которым нужно следить, и объект с "ловушками". "Ловушки" – это определенные функции, которые срабатывают при каком-либо действии с объектом. Например, ловушка get(<цель>, <ключ>) перехватывает получение объекта и выполняется вместо этого (аналогично тому, как работает геттер). Теперь, если мы напишем proxy.count, то сначала получим console.log, а затем само значение count. data.count продолжает работать, как обычно, и давать только значение. Также есть ловушка set(<цель>, <ключ>, <значение>), которая изменяет значение (аналог сеттера). Если мы сейчас напишем proxy.count = 4, то сначала получим console.log, а затем изменим значение count. При этом значение также изменится и в data.count (ну вы понимаете, ссылочная связь и т.п.). Надеюсь, вы уже поняли, как мы можем использовать эти ловушки для автоматического обновления наших данных. Давайте переименуем наш объект data в rawData, а data назовем наш объект с обозревателем. Всё! Теперь каждый раз при изменении свойств объекта data, это изменение будет перехватываться set() и вызывать refreshDom(). Откроем наше приложение в браузере.  Теперь, если написать в консоли данные в браузере поменяются сами! Вот она реактивность!  Итак, реактивность готова. Теперь последнее – хотелось бы иметь возможность нажимать на кнопки. Для регистрации прослушивателей событий мы будем использовать тот же самый паттерн, который мы использовали в refreshDom(), а именно: пройти по всем элементам DOM, найти все @click, зарегистрировать для них прослушиватели. Мы могли бы даже сделать это внутри функции refreshDom(). Единственная проблема – при каждом изменении данных прослушиватели добавлялись бы повторно, что сильно бы все усложнило. Поэтому для этого мы сделаем отдельную функцию. Запускаем мы ее единожды, после загрузки DOM. Готово! Теперь наши кнопки "+" и "-" работают. На этом статья подходит к концу. Мы полностью имплементировали работу Alpine.js для нашего конкретного куска HTML-кода. В последних двух видеоуроках курса на Laracasts рассказывается как добавить другие обработчики событий, такие как mouseenter и другие, как добавить другие директивы, а также, чем код, который мы написали отличается от реального кода Alpine.js. Эти видео входят в платную подписку Laracasts, поэтому не описаны здесь. Если вам интересно, ссылка есть выше. Однако, надеюсь, даже без них, вы смогли понять и попробовать, что это такое, написать собственный фронтенд фреймворк. Спасибо за внимание. Web-разработчик