Обновить

Комментарии 12

https://habr.com/ru/articles/968384/#comment_29145688

У веб компонентов много проблем архитектурных, давайте лучше на $mol писать, те же веб компоненты, только сильно прокачаннее

Видимо надо ещё немного подождать, пока они до конца устаканяться

Они уже давно это сделали)

Тёма интересная и, возможно, когда-то пригодится. Но, пока, реального профита от этого не видно. Возможно, если нужен очень сложный компонент, который используется и здесь и там, это пригодится.

А, в общем, спасибо за статью.

Превосходная статья. Добавил в закладки. Несколько вопросов.
1) судя по коду, вы не используете attributeChangeCallback. Не рассматривали ли Вы такой алгоритм.
Создаем общую функцию рендера, в которой мы сравниваем значения атрибутов, и если изменились, только тогда меняем часть DOM. Ну и плюс у нас однозначно будет какая-то общая логика. Т.е. что-то такое:
```

  render() {
    if (this.hasChanged("attr1")) {
      this.doSmth1(); // какая-то тяжелая операция
    }

    if (this.hasChanged("attr2")) {
      this.doSmth2();
    }

    // общая логика для любого рендера
  }

А в attributeChangeCallback мы добавляем батчинг операций с помощью очереди микрозадач. Что-то такое:
```

  attributeChangedCallback(
    name: string,
    oldValue: string | boolean,
    newValue: string | boolean,
  ) {
    if (oldValue === newValue) return;

    this.lastChangedAttr = name;
    queueMicrotask(() => {
      if (this.lastChangedAttr !== name) return;

      // prerenderAttrs - Map, в которой хранятся значения атрибутов 
      // (синхронизированных свойств) ДО рендера
      // сравнение изменений в функции рендера идет с соответстующим значение в нем
      const noChanges = [...this.prerenderAttrs.entries()]
        .every(([attr, value]) => this[attr] === value);
      if (noChanges) return;
        
      this.render();
      this.lastChangedAttr = "";
      this.prerenderAttrs
        .forEach((value, attr) => this.prerenderAttrs.set(attr, this[attr]));
    });
  }

2) насколько легко использовать adoptedStyleSheets при разработке? Насколько я помню, ссылку на соответствующий файл в девтулзах не найти (даже в FF). Не рассматривали ли использование какого-либо статического свойства 'defaultStyles`, в котором вы можете хранить, к примеру, тег style, ссылку на таблицу и т.п. Которое можно переписать на любом проекте (до инициализации компонента).

Превосходная статья. 

Спасибо, приятно слышать.

Не рассматривали ли Вы такой алгоритм.Создаем общую функцию рендера, в которой мы сравниваем значения атрибутов, и если изменились, только тогда меняем часть DOM.

Нет. Тут дело видимо в некорректных ассоциациях. Веб-компонент, это не реакт компонент, ему не нужно «ререндерится». Более того, сам ререндер в каком-то реакте тоже вводит в заблуждение. В реальности (на примере реакта) все устроено так: рендерится компонент, элементы вставляются в DOM, последующие ререндеры это изменение свойств/атрибутов уже вставленных в DOM элементов.

Тоже с веб-компонентом. То, что вы подразумеваете под рендером это вставка в DOM, и это происходит один раз – element.shaddowRot.innerHtml = someHtml. Это все. Изменение атрибута это просто изменение атрибута. При использовании с тем же реактом, это означает что произошли какие-то изменения, реакт ререндерит то, что у него называется компонентом, что в действительности приводит к вызову setAttribute у нашего элемента (и у других в разметке).

А в attributeChangeCallback мы добавляем батчинг операций с помощью очереди микрозадач. Что-то такое:

Это ничего не даст кроме дополнительной сложности или потребления памяти.

2) насколько легко использовать adoptedStyleSheets при разработке?

Никаких проблем. В нем хранятся стили перенесенные из lightDom, то есть их можно смотреть там. При дебаге (по крайней мере в хроме) adoptedStyleSheets доступны для просмотра.

Не рассматривали ли использование какого-либо статического свойства

Конечно, это упомянуто в статье: loadCssFromUrls и loadCssFromDocumentStyleSheets. Первый как раз вставит импорты соответствующих ссылок во внутренний <style>.

Тоже с веб-компонентом. То, что вы подразумеваете под рендером это вставка в DOM, и это происходит один раз – element.shaddowRot.innerHtml = someHtml. Это все.

Не совсем. Я не про connectedCallback. Я подразумеваю, такое. Допустим, мы делаем какой-нибудь date-picker. При переключениях месяцев у нас перестраивается таблица с числами. Или при нажатии на выбор месяца у нас открывается табличка с месяцем и годом. И вот, к примеру, за это отвечает атрибут view. При этом может меняться атрибут с отображаемым месяцем и годом. При одновременной установке нескольких атрибутов (если не батчить их) там будет выполняться довольно много операций.

При переключениях месяцев у нас перестраивается таблица с числами

Все еще обновление уже существующих элементов. То есть у нас есть таблица с днями, условные 6 рядов по 7 колонок, и при изменении месяца, изменяется textContent у некоторых ячеек, а у некоторых, дополнительно еще и атрибут disabled. В таком кейсе нет причин убить одни, уже существующие элементы что-бы создать набор таких же, только с другим текстом.

При этом может меняться атрибут с отображаемым месяцем и годом

Та же историю, только еще проще. Количество и названия месяцев не меняется от года к году.

Я правда не могу представить ситуацию, когда было бы нужно убить одну разметку и создать новую, и мне еще не приходилось реализовывать что-то вроде метода render в своих компонентах, но даже если представить что такое потребуется, то все равно нет смысла в усложнении. Не нужно недооценивать производительность DOM API, это не React. Вот пример с комбобоксом из статьи: stackblitz, тут в нем 5000 опций с jsonplaceholder, при чем опции изначально рендерит React, и тем не менее, он работает без каких либо лагов, причем в песочнице, хотя в нем нет никаких специальных оптимизаций, и тем более виртуализации. Попробуйте что-то поискать, выбрать пару значений и т.д., а после, чтобы совсем отсеять любые сомнения поставьте атрибут selected всем опциям:

return (
  <box-option 
    selected // !
  >
    {todo.title}
  </box-option>
);

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

Не нужно недооценивать производительность DOM API, это не React

У меня такое ощущение, что что угодно будет быстрее чем реакт )

Честно говоря, здесь вопрос больше не про производительность, а про удобство отладки. Именно отладка - самая большая боль.

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

 // в родителе
 elem.setAttribute("attr1", "val1");
 elem.setAttribute("attr2", "val2");
 elem.setAttribute("attr3", "val3");

// ну или так
 elem.attr1 = val1;
 ...
 elem.attr3 = val3;

В этом случае у нас будет 3 разных рендера (ну, можно назвать обновлениями DOM, чтобы не было путаницы). Особенно интересно, если в каждом колбеке на setAttribute будут меняться еще и другие свойства через тот же setAttribute... Отследить изменения становится довольно проблематичным.

И, допустим, мы столкнулись с багом и хотим понять почему у нас что-то не то отображается. В случае батча нам достаточно поставить debugger перед этим кодом и debugger в функции рендера, где мы уже точно сможем отследить текущее состояние атрибутов (синхронизируемых свойств) и понять откуда идет ошибка. Какое сочетание вызывает проблему.

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

Почему я это все спрашиваю? Я еще сам не определился в оптимальном подходе к веб-компонентам. Никто по сути еще не знает, как их правильно готовить. Поэтому чтобы эта отличная, на мой взгляд, технология получила распространение, нужно найти какой-либо общий подход. Чтобы вообще можно было отключить голову и следовать определенному шаблону. Пока что мои эксперименты показывают, что проще все-таки обрабатывать изменения в одной-единственной функции. На этом, собственно, и были построены классовые компоненты реакта. Кроме того, подход, что мы просто меняем свойства (по сути состояние), и, когда придет время (когда окажется, что больше ничего не вызывает синхронные изменений свойств), тогда уже рендерим (обновляем) - это довольно знакомый шаблон.

иногда проще полностью пересоздать заново группу элементов

React way

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

React way + очень странное утверждение. Каким образом изменение свойства уже созданного HTML элемента засоряет DOM?

проще все-таки обрабатывать изменения в одной-единственной функции

React way

На этом, собственно, и были построены классовые компоненты реакта

Который как вы выше сами признаете – неэффективен.

тогда уже рендерим (обновляем) - это довольно знакомый шаблон.

React way

Вы буквально пытаетесь воссоздать React, но в этом нет смысла. Нужно перестроить в голове ментальную модель.

elem.setAttribute("attr1", "val1"); elem.setAttribute("attr2", "val2"); elem.setAttribute("attr3", "val3");

Предположим, что мы это забатчим, что же мы получим? Ничего. Первый setAttribute меняет состояние элемента, и если это уместно, то произойдет какой-то painting или layouting. Потом второй, и процесс повторится. Потом третий. Какая разница, забатчили мы это или нет? Никакая. На любой вызов setAttribute, не важно откуда, браузер сделает свои дела, только в предлагаемом вами варианте мы добавляем дополнительную сложность без какого-либо профита.

В том же дейт-пикере нет особой необходимости хранить таблицы годов и месяцев в DOM и скрывать их через стили.

Никто так и не делает, разумеется. Там просто пара элементов в которых просто меняется контент.

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

Вы предлагаете создать состояние, и периодически синхронизовывать его с DOM. Это тот же React way. HTMLElement это уже состояние. Вызвав setAttribute или elem.prop = value мы как раз его изменяем, а браузер сделает свои дела, когда посчитает нужным.

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

Вы предлагаете создать состояние, и периодически синхронизовывать его с DOM. Это тот же React way. HTMLElement это уже состояние.

Нет, я предлагаю сделать обычные геттеры-сеттеры. Что-то вроде этого:

  get view() {
    return this.getAttribute("view");
  }
  set view(value) {
    this.setAttribute("view", value);
  }

Не имеет смысла хранить что-то в дополнительном состоянии объекта элемента. Впрочем, ладно, может быть и имеет смысл именно только в точечных изменениях.

Я проверил подход с атомарной установкой свойств. Тоже неплохо. Но он чуть сложнее в разработке. Нужно думать, как свойства взаимодействуют друг с другом в каждом сеттере. Некоторые свойства, изменения которых при некоторых условиях должно вызывать изменения других, следует объединять в одно во избежания ненужных лишних обновлений.
Ну и начальный код, который я написал на коленке тоже несколько переусложнен
Достаточно вот этого (правда, обновление будет и при такой установке elem.attr = newValue; elem.attr = oldValue;, но такой код писать точно не стоит, поэтому уберем сравнения со свойствами до рендера):

// ряд геттеров-сеттеров
get attr1() {
  return this.getAttribute("attr1") || "defaultValue";
}
set attr1(value: unknown) {
  // опциональная валидация
  this.setAttribute("attr1", value);
}

attributeChangedCallback(
  name: string,
  oldValue: string | boolean,
  newValue: string | boolean,
) {
  if (oldValue === newValue) return;

  this.pendingUpdates.add(name);
  if (this.pendingUpdates.size > 1) return;

  queueMicrotask(() => {
    // одна-единственная функция, записанная в очереь
    // будет отменена при императивном вызова update
    if (this.pendingUpdates.size) this.update();
    this.pendingUpdates.clear(); // защита от дурака
  }
}
  
update() {
  if (pendingUpdates.has("attr1")) {
    // логика точечного обновления
  }

  // общая логика для всех обновлений

  // На всякий случай очистка при возможном императивном вызове
  this.pendingUpdates.clear();
}

Это будет работать значительно проще и надежнее, нежели атомарные изменения при установке свойств.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации