Комментарии 37
Видите ли в чем дело… Как бы объяснить. Тут, конечно, на любителя, но я привык что разметка — она все-таки делается в HTML. Используя подходы в духе того, что предлагает d3js, вы как бы меняете свой язык разметки с привычного глазу HTML на DSL, в виде системы функций d3js. То есть по сути вы, как человек и как пароход, выполняете работу HTML-парсера. Если конвертация HTML в набор вызовов не происходит автоматически, аки в tsx — то как по мне поддерживать подобный код — весьма больно (а в ситуации с моим фреймворком такой подход и вовсе не применим — я пытался). Но, конечно, дело вкуса.
Наверное зависит от проекта. Если пользователь активно взаимодействует с содержимым страницы — перетаскивает какие-то узлы и т.п., то перегенерировать целиком и обновлять каждый раз HTML очень накладно.
Ещё мне в принципе не нравятся шаблоны на клиенте. Понимаю на сервере шаблоны для страницы целиком. Но для клиента поддерживать кучу мелких HTML-фрагментов не очень.
Самое ужасное последствие от использования innerHTML — это то что внутреннее состояние DOM элементов будет потеряно (цсс анимации, видео, аудио, позиция курсора в полях ввода, позиции скролла и много других состояний).
> Парсер HTML… Но упомянуть его определенно стоит, во многом потому что он нам понадобится для реализации VDOM.
Совершенно ненужная вещь, одна из основных фич vdom'а — это то что все узлы виртуального дома являются обычными жаваскрипт значениями и с ними можно работать так как душе угодно, никак не ограничивая себя примитивным шаблонизатором.
> Как я понимаю, React.js работает без парсера HTML в виду того, что его шаблоны (jsx/tsx) уже собираются в соответствующие вызовы создания нод.
Это не шаблоны, это обычный жаваскрипт и синтаксический сахар вокруг вызова `createElement`.
> По сути дела надо взять parent-нод, внутри которого отрисован наш HTML, взять массив всех его детей и сравнить его с аналогичным массивом виртуальных детей, который нам создал HTML-парсер.
Так работают медленные vdom реализации, все быстрые реализации производят все сравнения на vdom структурах.
> LCS — и есть самое сложное в VDOM-е, из-за чего многим он кажется страшной магией.
Единственная известная мне реализация vdom'а которая использует LCS на сегодняшний день — это github.com/yelouafi/petit-dom. Я перестал использовать LCS для решения этой проблемы ещё 4 года назад, github.com/ivijs/ivi/blob/2c81ead934b9128e092cc2a5ef2d3cabc73cb5dd/packages/ivi/src/vdom/implementation.ts#L1366-L1593 здесь можете ознакомиться с моим решением этой проблемы.
И подавляющее большинство vdom библиотек не замарачиваются и используют примитивный линейный алгоритм, из-за чего в том же реакте простая перестановка элемента может привести к N-1 `insertBefore` операций.
Совершенно ненужная вещь
Зависит от того, что у вас на входе. Если больше чем сырой HTML вы получить не можете (мой случай) — придется парсить. Ну и да — пущай HTML-парсер все же будет в этой статье. Некоторым таковой нужен для своих тёмных делишек — так пусть же лучше от меня узнают, чем от какого-нибудь… соавтора jQuery :)
Это не шаблоны, это обычный жаваскрипт и синтаксический сахар вокруг вызова createElement
.
Я не спорю, но выглядит скорее как шаблоны.
все быстрые реализации производят все сравнения на vdom структурах
Если вы имеете в виду immutable-структуры, то согласен. В противном случае не очень понимаю о чем речь.
здесь можете ознакомиться с моим решением этой проблемы
Я погляжу, большое спасибо.
не замарачиваются и используют примитивный линейный алгоритм
Очень странно. LCS — первое, что пришло лично мне в голову.
Для vdom'а на входе примитивная деревяшка жаваскрипт объектов, которая не требует каких-то особых шаблонизаторов или парсеров.
> Если вы имеете в виду immutable-структуры, то согласен. В противном случае не очень понимаю о чем речь.
Имею в виду что DOM объекты трогают лишь в случае если нужно производить какие-то изменения, для сравнения используют только виртуальный дом.
> Очень странно. LCS — первое, что пришло лично мне в голову.
Да, мне тоже когда-то давно это первое что пришло в голову, и моя первая реализация использовала LCS, но потом началась жёсткая борьба в попытках получить лучшие цифры в бэнчмарках и пришлось искать как можно ускорить всё это дело под большинство реальных кейсов.
примитивная деревяшка жаваскрипт объектов, которая не требует каких-то особых шаблонизаторов или парсеров
В вашем случае — да. В моей задаче (фреймворк для датагридов) — увы, нет. :(
DOM объекты трогают лишь в случае если нужно производить какие-то изменения
Тоже хорошее допущение, однако лично мне недоступное. В рамках статейного примера, конечно, можно было бы, но в нутрях фреймворка я, к сожалению, не могу сохранить VDOM-дерево после предыдущей перерисовки для последующего сравнения :(
Так вы же пытаетесь описать vdom в этой статье и парсер HTML ну совсем не имеет никакого отношения к vdom и лишь наоборот только вводит в заблуждение тех кто ещё не до конца понимает в чём заключается одна из его ключевых особенностей.
Когда термин «виртуальный дом» стал массово использоваться, были даже дискуссии среди вдом авторов на тему того что сам по себе термин совершенно не в тему, и предлагались идеи вроде «Value DOM».
Я хочу не "описать vdom" — его из без мой помощи уже порядочно… описали :) Я хочу показать пример, как можно не имея ничего сделать конкретное приложение, использующее VDOM и потрогать его руками. Для этого, увы, сферического в вакууме VDOM-а недостаточно. Городить сотню функций для каждого тега вижу неиллюстративным, подобие tsx — дюже трудозатратным. Остаётся HTML-парсер.
Надо где-то написать крупными буквами, что он отношения к VDOM самому по себе не имеет.
hyperscript можно реализовать в десяток строк, используется в куче vdom либ для тех кто не любит jsx.
mithril.js.org/hyperscript.html
Если больше чем сырой HTML вы получить не можете
Как минимум, у вас на входе есть уже подготовленный браузером DOM. Сериализовать его путем вызова element.innerHTML
, чтобы затем начать его героически парсить — это как-то странно
Самое ужасное последствие от использования innerHTML
Для меня самым ужасным и несовместимым с жизнью было то, что теряются данные в пользовательских input-ах.
Это не шаблоны, это обычный жаваскрипт и синтаксический сахар вокруг вызова createElement
.
Вы так пишите как будто шаблон не может быть обычным жаваскриптом или синтаксическим сахаром...
При поверхностном взгляде кажется, что нужно просто сравнить два дерева и произвести минимальные изменения для трансформации одного в другое. И это действительно так, когда мы полностью контролируем состояние. К сожалению, как выше отметили, далеко не все состояния элементов нам подконтрольно. Поэтому мы не можем рендерить одни и те же данные то в одни узлы, то в другие. То есть узлы должны однозначно соответствовать исходным данным. В Реакте для этого есть костыль с указанием key для элементов полученным из массивов. Но эта проблема куда фундаментальней. key не поможет при переносе элемента из одного массива в другой, из одного контейнера в другой и тд. И самое печальное, что идентифицировать элементы невозможно автоматически — только программисту известно где перенос, а где удаление и добавление чего-то похожего. Поэтому в моих реализациях программист всегда явно задаёт уникальное имя каждому элементу исходя их данных, что он рендерит. Например: https://github.com/eigenmethod/mol/tree/master/dom/jsx
Все остальные костыли хуже.
> key не поможет при переносе элемента из одного массива в другой, из одного контейнера в другой и тд
При переносе дом узлов всё равно потеряется внутреннее состояние, которое невозможно никак иначе контроллировать. А там где с этим нет проблем flutter.io, там можно задавать глобальные ключи для переноса между контейнерами.
Можно и без костылей.
Какое-то потеряется, какое-то нет. Но тут есть более стрёмный момент — при рендеринге других данных в тот же узел его внутреннее состояние останется от старых. Например, удалилась строка где-то выше и фокус сместился на другие данные. А у глобальных идентификаторов куча плюсов и помимо возможности переносить узлы куда угодно:
- понятна его семантика
- понятно кто его владелец
- быстрый доступ из консоли
- не ломающиеся при каждом чихе тесты
- ну и никакие дифы считать не надо, элементы располагаются за линейное время
mayorovp это совсем не трудоёмкая операция, если используются правильные инструменты.
Поэтому в моих реализациях программист всегда явно задаёт уникальное имя каждому элементу исходя их данных, что он рендерит.
Замечательно, самую трудоемкую операцию взяли и объявили ручной.
В Реакте для этого есть костыль с указанием key для элементов полученным из массивов.
Это не костыль. Это единственно верное решение, которое не приводит к нестабильному и потенциально некорректному поведению.
1. За элементом VDOM может стоять компонент с каким-то состоянием.
2. Элемент VDOM привязан к DOM-узлу с определенным unmanaged состоянием.
3. Если у вас есть несколько похожих по типу элементов VDOM подряд, то это с большой вероятностью значит, что там цикл, а где цикл — там список объектов. Таким образом VDOM элемент с большой вероятностью привязан к объекту.
Исходя из этих пунктов вытекает следующее: некорректно проставленный key может испортить текущий стейт неопределенным образом. Это было бы не критично, если бы у вас весь стейт был бы глобальным деревом, которым управляете вы. Но в том же DOM есть очень много внутреннего состояния, которое мы не можем программным образом контролировать.
Поясню более детально. Кейс:
1. В массиве есть один объект.
2. Объект меняется. А может даже вместо него создается копия с модифицированными полями — т.е. на ссылки полагаться нельзя.
3. Добавляется еще один объект.
Нельзя просто так взять и идентифицировать объект — нужен айдишник. А айдишник может отсутствовать. Или может быть записан в id, Id, key, Key, isoCode или еще какое-то упоротое поле. А значит нельзя понять соответствие внутреннего состояния между старыми компонентами (или DOM-узлами) и новыми компонентами. Терять это внутреннее состояние мы не можем: пользователь или может этого не хотеть, или может не мочь восстановить это состояние сам.
Этот кейс вылазит в следующих случаях:
1. У вас данные из одного объекта имеют сложную связь с его местоположением в массиве. Например, первый объект не может иметь каких-то свойств. Это например, цепочка ответственности отображенная на UI. Кейс редкий, да :-)
2. У вас перерисовка происходит батчами и стейт успевает поменяться два раза (например с сервера по Websockets приходит все, что юзер пропустил будучи оффлайн)
Благо, задача построения diff-а в программировании довольно известная — гуглится, внезапно, по словам нахождение наибольшей общей подпоследовательности. Решается она динамическим программированием. Алгоритм имеет квадратичную сложность.
Вы не до конца разобрались в этой теме «диффинга». LCS который имеет O(n^2) сводится (для случая когда элементы не повторяются) к LIS (longest increasing subsequence) которая имеет сложность n*log(n) и реализуется как часть «терпеливой» сортировки. Алгоритм примерно такой — создаем временный массив и проходимся по элементам нового массива — вытаскиваем элемент по ключу «key» и смотрим какой был индекс в предыдущем массиве. Если индекс больше индекса элемента который мы обработали до него то добавляем его в конец этого временного массива и в отдельном свойстве «prev» записываем элемент который стоит перед ним в временном массиве а если индекс меньше — то ищем бинарным поиском элемент с наименьшим индексом который больше нашего. По достижению конца списка смотрим на последний элемент во временном массиве и проходимся связанному списку «prev» — это и будет наибольшая возрастающая последовательность. Кстати в отличии от inferno, ivi и прочих этот алгоритм можно еще сильнее оптимизировать если не создавать новые временные массивы а переиспользовать один глобальный каждый раз при diff-е и уменьшить количество проходов — на первом проходе формируем тот временный массив а на обратном сразу можно предпринять перестановку элементов — не дожидаясь формирования конечного lis — то есть считываем свойство «prev» — если элемент равен текущему элементу в новом массиве то его не трогаем. Если не равен то нужный элемент вставляем в текущее место через insertBefore.
В 99% случаев в реальном приложении при апдэйте списков не придётся никого переставлять, поэтому усложнение предпроходов может сказаться на производительности более частых случаев. Переиспользование массива точно не даст никакого прироста.
Переиспользование массива точно не даст никакого прироста.Меньшее создание объектов будет меньше требовать cpu-времени на создание а потом на вызов сборщика мусора
Слишком много всего написано, и ни одного примера кода.
А еще не помешало бы демо, которое можно открыть в браузере и поиграться. Ссылка на github напрямую не заработает, потому что исходники в typescript.
А то есть вы ссылку на jsfiddle вначале статьи не увидели и клонировать репозиторий на github не можете?
Беда.
VDOM своими руками