РЕДАКТИРОВАТЬ: 11/2017 - Первоначально это сообщение было опубликовано 04/2016 . У Иегуды Каца теперь есть предложение WHATWG для« DOMChangeLists », которое очень похоже на поведение Node # mergeWith (), описанное позже в этом посте, хотя это не было t вдохновлен этим сообщением.

LazyDOM - эксперимент по внедрению виртуальной DOM в браузер изначально

tl; dr Виртуальные элементы в стиле React бывают быстрыми, но их нельзя использовать, как настоящие элементы, элементы LazyDOM быстры, как элементы React, но их можно использовать как настоящие элементы DOM, потому что они лениво проксируются на один из них. Чтобы было ясно, это всего лишь эксперимент!

Давайте начнем с обманчиво простого примера, чтобы дать вам представление о том, что дает вам LazyDOM. Это может показаться наполовину знакомым, но это не React, это LazyDOM (с использованием необязательного JSX).

Примечание: хотя я использую здесь jQuery, на самом деле речь идет не о jQuery.

React, возможно, популяризировал концепцию виртуальной модели DOM и однонаправленного потока данных, которая произвела революцию в разработке пользовательского интерфейса. Это действительно здорово. Проблема в том, что его подход не совсем естественен для нативного принятия в спецификации W3C, которую реализуют браузеры. Во-первых, эргономика разработчика вокруг виртуальной DOM в некоторых местах неудобна. Виртуальные элементы - это на самом деле просто обычные старые объекты JavaScript (POJO), поэтому для интеграции с такими вещами, как плагины jQuery, требуются escape-штриховки, и касание реального DOM таким образом также приводит к несогласованному состоянию DOM. Все это затрудняет изучение и отладку, особенно для новичков, которые просто хотят получить преимущества без необходимости изучать технические предостережения.

LazyDOM позволяет избежать этих проблем, взаимодействуя с вашими «ленивыми» узлами не иначе, как с реальными узлами DOM, при этом позволяя вам сохранить модель «все заново отрисовывать».

Простое введение

Без лишних слов, вот как это выглядит:

Скучно, да? Продолжайте читать, чтобы узнать о сочных вещах!

Создание ленивых элементов и текстовых узлов выполняется почти так же, как и их настоящие аналоги:

Для простоты я часто использую термин LazyNode для обозначения узла LazyElement или LazyText.

Если у вас есть LazyNode, вы можете получить доступ к тем же свойствам и методам, которые вы ожидали бы найти, если бы они не были LazyNode. Так, например, настоящий ‹input› (также известный как HTMLInputElement) имеет свойство value, и вы можете прослушивать изменяется с помощью oninput.

Если вы написали необработанную модель DOM на JavaScript, все это должно быть вам хорошо знакомо, так в чем же дело со всей этой ленивой вещью и почему она лучше?

Эта проблема

Магия LazyDOM откроется, когда вы захотите выполнить повторный рендеринг. Если бы вы хотели использовать популяризованную в React парадигму «перерисовывать все», как бы вы сделали это с чистыми, реальными узлами DOM? (он же обычный DOM API без фреймворка) Вот наивный пример:

Если вы запустите этот код, вы заметите, что как только вы введете один символ в поле ‹input›, вы потеряете фокус. Это потому, что мы буквально воссоздали все узлы, поэтому, конечно же, поле ввода, которым мы его заменили, является совершенно новым элементом. Это намеренно наивный пример, потому что это могло быть очевидно не для всех. Помимо проблем с фокусом и выбором, создание таких реальных узлов DOM может быть дорогостоящим как по времени создания, так и по необходимости перерисовки всей страницы.

Так как же добиться желаемой эргономики разработчика, но избежать этих проблем? Вы угадали, LazyDOM:

Решение

Разница настолько тонкая, что ее легко упустить, попробуй. Мы переключаемся на использование createLazyElement, а при последующих повторных отрисовках мы используем новый метод Node # mergeWith () для примените новое ленивое дерево различий к первому. Этот метод существует только на LazyNode’s (подробные технические подробности можно найти в отдельном посте). Этот пример также очень надуман для простоты, см. Ниже более сложные примеры или не стесняйтесь играть в JSBin.

Первое созданное нами дерево LazyNode будет добавлено в document.body, и в это время каждый LazyNode внезапно создаст реальные узлы из поставленных в очередь изменений свойств и дочерних элементов.

При последующих повторных отрисовках с использованием mergeWith вместо создания новых реальных узлов DOM для каждого LazyNode внесенные в очередь изменения применяются к существующим узлам, которые находятся в DOM. , на месте и, что важно, новые ссылки на дерево теперь указывают на существующие узлы в DOM, поэтому поиск input.value внутри input.oninput работает должным образом.

Построив очередь, вы можете подумать, что у LazyNode есть ловушка установщика, которая запускается для каждого свойства, которое вы назначаете на LazyNode:

Поскольку наш пример InputBox не считывает никаких свойств в LazyElement, вот пример, который:

Выше мы читаем input.value сразу, а затем снова, когда запускается обработчик oninput. Под капотом LazyNodes есть универсальный геттер, который выглядит примерно так:

При чтении свойства на LazyNode существует три основных ситуации:

  • Вы ищете свойство, которое вы настроили ранее (хранится в queuedProperties).
  • Настоящий узел существует, и мы должны просто найти его (что чаще всего происходит после того, как вы вставили LazyNode в документ или слили его со старым деревом)
  • (худший случай) Вы ищите свойство на LazyNode, которое не может быть определено без существующего реального узла, поэтому один из них создается лениво точно в срок. Чтение свойств, которые деоптируются подобным образом (которые создают тот узел, который в конечном итоге может быть выброшен), действительно редко, поэтому мы можем обеспечить такое удобство разработчика с небольшими реальными затратами.

Аналогичный механизм организации очереди используется, когда вы lazyNode.appendChild (node) и т. Д.

Использование JSX

В реальных приложениях вы, вероятно, будете использовать JSX, чтобы сделать вещи более декларативными, как и в React. Поскольку вероятность того, что JSX когда-либо будет изначально поддерживаться в JavaScript, еще меньше, довольно тривиальный плагин Babel может выводить соответствующий императивный код:

По соображениям производительности было бы очень удобно, если бы document.createLazyElement принимал сигнатуру, аналогичную React.createElement (tagName, props,… children) , но это не является обязательным и выходит за рамки данной публикации.

По сравнению с существующим React

Существующий React использует виртуальные элементы, которые являются просто POJO:

Это очень быстро, но очевидно, что они не могут быть использованы чем-либо, ожидающим настоящего узла элемента. Вместо этого вы используете refs и различные escape-штриховки, чтобы получить ссылку на реальный узел после согласования. Вот надуманный пример, поэтому его легко понять:

Некоторые могут предположить, что эти предостережения React обеспечивают чистоту разделения задач, что в некоторой степени верно, но для примитивов браузера это довольно самоуверенное применение. На практике полезно иметь возможность относиться к вещам так, как будто это просто DOM, чтобы люди могли делать свои собственные предпочтения (или нарушать их), чтобы все заработало и отправило его . Чисто умозрительно, но такой примитив, как LazyDOM, может предоставить новые и полезные шаблоны, которые в противном случае не появились бы. В конце концов, React был создан путем переосмысления устоявшихся лучших практик.

Ленивое слияние деревьев

Когда вы объединяете ленивое дерево с другим, оно использует входящее дерево как diff / patch, как и следовало ожидать, но каждый соответствующий узел в новом дереве теперь внутренне ссылается на исходные, реальные узлы DOM. Это может быть немного сложно понять, но эффект является наиболее важным: новые LazyNodes фактически будут ссылаться на исходные узлы DOM, которые вы визуализировали в первый раз, сразу после их объединения. LazyNode - это фактически ленивые прокси. Когда они могут, они будут ставить в очередь вносимые вами изменения, такие как установка свойств и добавление дочерних элементов, а затем применять их к реальному узлу только тогда, когда это необходимо.

Это может помочь вам узнать, что на самом деле эталонная реализация (полифил) LazyDOM действительно использует ES2015 (ES6) Prox y, что не является широко известной функцией. Многие думают, что они медленные, но на самом деле они чертовски быстрые!

Хотя обычно в этом нет необходимости, вы можете вручную повторно установить (сделать реальный узел DOM существующим), получить к нему доступ и даже переназначить их с помощью свойства node. Это то, что mergeWith делает под капотом после применения diff, так что новые узлы ленивого дерева ссылаются на исходные реальные узлы.

Вот некоторые из случаев, когда реифицируется реальный узел:

  • Вы размещаете LazyNode в другом реальном Node, например, когда вы document.body.appendChild (lazyNode)
  • Вы читаете свойство, для которого требуется реальный узел, но его еще нет, например clientWidth и, вероятно, многие другие, хотя большинство из них можно оптимизировать для возврата ожидаемых значений; например, clientWidth всегда равен нулю, если элемент еще не находится в DOM.

А как насчет реальных приложений?

Хороший вопрос. Хотя на данный момент это просто забавный эксперимент, я поиграл с приложениями разной степени сложности. Вот пара, хотя, по общему признанию, они еще не полностью проверили жизнеспособность в реальном мире.

LazyDOM не является полноценной структурой пользовательского интерфейса или библиотекой, как React, поэтому здесь нет встроенной концепции setState или хуков жизненного цикла; это то, что вы можете построить поверх примитивов LazyDOM.

Так что имейте в виду, что эта эталонная реализация LazyDOM собрана вместе, а не на самом деле оптимизирована или протестирована в боевых условиях. По этой причине на данный момент я не предоставляю неулучшенный исходный код. Вы можете играть в JSBin на свой страх и риск. Безусловно, существует множество ошибок, о некоторых я знаю, а другие исправляются за годы исследований в реальном мире. Было бы довольно неприятно проводить какие-либо конкретные сравнения производительности на данный момент, за исключением общего утверждения, что это действительно быстро. React содержит сотни исправлений ошибок и несоответствий браузера, поэтому я хотел прояснить, что я не предлагаю смерть React или что-то подобное.

Альтернативы

Наиболее очевидная альтернатива, к которой приходят некоторые люди при рассмотрении этого, - «почему настоящие узлы Element / Text не могут просто реализовать лень в качестве детали реализации, не связанной со спецификацией?». Действительно, они могли бы - с степень. Все, что делает создание узла дорогостоящим, можно делать лениво, или они могут даже обернуть свою внутреннюю логику почти той же логикой прокси, которую я использую в эталонной реализации. Даже если бы они это сделали, это не удовлетворило бы это предложение, поскольку я выступаю за способ изменить узел, на который нацелен прокси, чтобы новые ленивые деревья можно было объединить с существующими деревьями на месте. Эта возможность либо должна поддерживаться существующими узлами Element / Text, либо она может быть у новых вариантов LazyElement / LazyText.

Еще одна вещь, на которую можно обратить внимание, - это метод mergeWith, который выполняет фактическое сравнение и выполняет newLazyNode.node = oldLazyNode.node , чтобы новые узлы дерева указывали на существующие узлы. Хотя стандартизация, безусловно, выиграет от этого, детали, вероятно, более спорны и (относительно) легко справятся с библиотеками. Спецификации должны быть сосредоточены на примитивных строительных блоках, и я вижу, что mergeWith рассматривается как слишком самоуверенный.

РЕДАКТИРОВАТЬ: 11/2017 - Иегуда Кац теперь имеет предложение WHATWG для« DOMChangeLists », которое по духу очень похоже на Node # mergeWith (). Технические характеристики меняются медленно, но достигнутый прогресс обнадеживает.

Это просто эксперимент!

Наконец, я хочу уточнить, что это всего лишь технический эксперимент. Я не имею прямого отношения к W3C или команде React, и я не планирую поддерживать эталонную реализацию как реальную библиотеку. Я просто подумал, что это интересная идея и есть над чем поэкспериментировать. Некоторые могут возразить, что использование API браузера - неправильный подход, и я не возражаю; он может существовать как отдельный фреймворк, как React, я просто не в восторге от перспективы создания еще одного фреймворка.

Шансы на то, что это когда-либо станет настоящим стандартом браузера, очень малы, даже если в конечном итоге это окажется полезным подходом. Пожалуйста, не берите код и не используйте его в продакшене и не жалуйтесь на ошибки :)

«Ленивая», а не «виртуальная» модель DOM

Возможно, сейчас самое время пояснить, что я не уверен, что это «виртуальный DOM». Насколько мне известно, это новый подход, поэтому в настоящее время я называю этот метод «ленивым DOM», отсюда и название. Я просто сохранил это «виртуальное» модное слово из-за очевидного сходства, которое у них есть, и того, что люди знакомы с ним.

  • Для простоты в этом посте опущены многие детали (например, как будет работать ленивый SVG, addEventListener и т. д.), это не является исчерпывающим, поэтому сведите троллинг и «ну на самом деле!» к минимуму, но конструктивная критика по-прежнему приветствуется.

ОБНОВИТЬ:

Еще пара примеров, которые люди просили:

  • Веб-компоненты: http://output.jsbin.com/huhuxo. Повторный рендеринг внутри компонента немного неудобен без фреймворка привязки данных, но это не связано с LazyDOM. Вы можете использовать Polymer (если он не касается чего-то, для чего я не добавил поддержку в демонстрации)
  • Плагин jQuery UI accordion: http://jsbin.com/pezesu/edit?js,output