Всем привет! В этой статье я хотел бы поделиться своими мыслями о том, почему виртуального DOM можно избежать при создании реактивности сегодня. Я работаю со всем этим уже около полутора лет, создавая фреймворк Cample.js, и у меня есть некоторые соображения по этому поводу.
Возможно, я ошибаюсь. Поэтому, если вам не сложно, вы можете поправить меня в комментариях.
С того момента, как я начну говорить о различиях, я хотел бы написать о том, что такое “реактивность” на самом деле.
Реактивность – это когда представление привязано к данным таким образом, что изменение данных вызывает изменение в самом представлении.
Простой пример реактивности может быть реализован с помощью Proxy
, когда мы записываем некоторую информацию в объект и, в зависимости от этого, будет вызвана функция, которую мы описали.
const obj1 = { value: "Text", }; const el = document.createElement("div"); const updateView = (value) => { el.textContent = value; }; updateView(obj1.value); const proxy = new Proxy(obj1, { set: (target, prop, value) => { if (prop === "value") { updateView(value); target[prop] = value; } return true; }, }); console.log(el.innerHTML); proxy.value = "Text 1"; console.log(el.innerHTML);
Вы можете реализовать это действие без Proxy
, но в целом этот пример показывает, как все это примерно работает. Вы можете добавить сравнение со старым значением (oldValue !== value
), но тогда вы можете начать искать и внедрять тему глубокого сравнения переменных и тому подобное, и тогда пример будет намного сложнее.
В общем, главная идея заключается в том, что мы изменили значение объекта, оболочка (Proxy
) перехватила событие и вызвала функцию, которую мы описали.
Теперь давайте представим, что мы создаем веб-сайт. У сайта есть собственное DOM-дерево элементов.
Пример DOM дерева
И для этого в javascript мы можем создать объект, в котором реальный DOM будет отражен через вложенность. При изменении данных мы можем сначала обновить этот объект, а затем обновить сам DOM, но есть нюанс.
const a = { tag:"DIV", childNodes:[...], listeners:[...], atrributes:[...] }
Нюанс в том, что это может быть медленно и вообще-то бессмысленно. По сути, что подразумевается под “обновлением представления”? Если перечислять действия, которые выполняются, то по сути это замена одного узла другим, его удаление и добавление. Обновление текста узла и обновление атрибутов узла, а также работа обработчиков событий с новыми данными и, собственно, это всё. Что еще нужно для обновления DOM на веб-сайте? Если вся работа будет проходить через фреймворк и никакие сторонние библиотеки внезапно не добавят свои узлы в DOM, то, по сути, этого будет достаточно.
Вам не нужно работать с виртуальным DOM. Почему сначала нужно обновить его, а затем внезапно обновить реальный DOM при таких условиях? Разве невозможно немедленно обновить реальный DOM, просто сравнив данные?
Ну, на самом деле возможно. И уже давно, например, 4 или 5 лет назад, уже существует куча фреймворков или библиотек, которые обходятся “без” работы с виртуальным DOM.
Нет, то есть они не работают с ним в устоявшемся смысле, когда сравниваются теги и другое – нет. Суть в другом. Сейчас есть один понятный практически всем паттерн – это создание шаблона компонента.
Итак, вот где используется виртуальный DOM. И он там самый крутой, потому что помогает найти самый быстрый способ сборки DOM-узлов.
Короче говоря, представьте себе такую ситуацию. У вас есть строка содержащая HTML-код, и вам нужно создать 1000 DOM-узлов по её шаблону.
const stringHTML = "<div>123</div>";
Самый простой способ сделать это – создать из строки один DOM узел и непрерывно клонировать его. То есть, точно так же, как с class
в javascript, когда вы создаете 1000 новых экземпляров класса.
Примерно так это будет выглядеть в коде:
const doc = new DOMParser().parseFromString(stringHTML, 'text/html'); const el = doc.getElementsByTagName("div")[0]; const newEl = document.createElement("div"); for(let i = 0; i < 1000; i++) newEl.appendChild(el.cloneNode(true));
И вот тут-то виртуальный DOM и пригодится для создания наилучшего шаблона! Вы, конечно, можете это сделать так, но это будет ужасно медленно, потому что алгоритм создание ссылок на DOM узлы будет работать как простой перебор до нужного:
for(let el of document.querySelectorAll("*")){...}
То есть, подводя итог, основной посыл заключается в том, что виртуальный DOM – вещь необходимая, но не всегда нужная в реактивности. Сегодня textContent
и setAttribute
могут работать довольно хорошо без обновления сначала виртуального DOM, а затем реального.
Да, с виртуальным DOM происходит очень быстрая реактивность, но сам факт, что вы сначала обновляете объект, а только потом DOM, не всегда крут, потому что это еще одно действие в коде, которое не всегда необходимо, если только вы не проходите через узлы, которые были добавлены где-то снаружи фреймворка или библиотеки.
В целом, мы также можем упомянуть о существовании “технологии keyed”, когда задаваемый ключ из данных (и не только) привязан к узлам, но в этом смысле разница в реактивности вообще не связана, потому что это уже другая концепция.
Сегодня, для обновления по минимуму DOM, достаточно всего лишь объекта со следующими свойствами:
1. Ссылка на DOM узел
2. Ссылки на те дочерние узлы, которые содержатся в узле
3. Старые значения обновляемых данных в DOM
4. Ключ (опционально)
И, в принципе этого достаточно. Не нужен тег, какие-то объекты лишние и т.д. Понятное дело, что это всё поверхностно, но, в целом, это как-то так.
Всем большое спасибо за прочтение статьи!