Отказ от ответственности: я не следовал стандартной компонентной модели для Интернета, она даже близко не надежна для производства, название может меня немного ввести в заблуждение, а производительность меня не заботила… вообще!

Цель: написать Javascript-библиотеку, которая могла бы давать почти такой же результат (тот же? Правда? Немножко претенциозный?), что и Angular или React, а значит имеет:

  • состояние: экземпляры, которые запускают новые рендеры при обновлении его значений;
  • компоненты: основные объекты наследуются от одной и той же «компонентной структуры» и совместно используют функции рендеринга, конструкторы, деструкторы (не нативные для JS), управление событиями, сборщик мусора (связанный только с областью действия компонента, поскольку в JS уже есть сборщик мусора), синтаксический анализ HTML , и т.д. (= возможно, я добавлю сюда что-то позже);
  • HTML-код «отделен» от JS-кода.

TL:DR: Фрагменты HTML создаются из каждого экземпляра компонента, вызываемого из узлов, расположенных ближе к корневому узлу; каждый раз, когда «какой-то HTML» не отображается, это означает, что по крайней мере один экземпляр компонента не нужен, поэтому мы отфильтровываем его из списка всех экземпляров компонента. Самые важные функции не являются методами класса:

1 — replaceComponentDeclaration: отвечает за построение DOM, вызывается из корня;

2 — parseForEvents: вызывается для отображения ВСЕХ событий, прикрепленных к тегам HTML внутри строки, возвращенной из (1);

3 — getAllEvents: вызывает (2), затем устанавливает addEventListener в теги HTML.

Состояние

В ходе этой работы у меня появились крутые инсайты, которыми я решил поделиться с теми, кому нечем заняться. Вы можете проверить полный исходный код здесь (вся библиотека представляет собой 300-строчный файл component.js; component.js — это всего лишь одно приложение). Начнем с управления состоянием. По сути, это класс, хранящий необработанные данные и вызывающий новый рендеринг (ранее установленный для компонента) при каждом его изменении.

class State {
  constructor(render, initialState) {
    this.render = render;
    this.state = initialState;
  }
  
  set = (cb) => {
    if (cb instanceof Function) {
      console.log("Old state: ", this.state);
      this.state = cb(this.state);
      console.log("New state: ", this.state);
    } else {
      this.state = {
        ...this.state,
        ...cb
      };
    }
    this.render();
  }

  get = (key) => {
    return this.state[key];
  }
}

Метод state.set может принимать как объект, так и функцию-редуктор в качестве аргумента.

Базовая структура компонента

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

class Parent extends Component {
  static HTMLTagWrapper = "tr";
  constructor(key, props){
    super(key, props);
    this.state.set({ count: 0 });
  }
  
  handleButtonClick = (key = "") => {
    // key === "times"
    this.state.set(state => ({ ...state, count: state.count+1 }));
  }

  renderHTML = () => {
    const contents = `
      <td>
        ${this.props.name}
      </td>
      <td>
        ${this.props.factor}
      </td>
      <td>
        <button event-click="${this.id}/times">multiply by ${this.state.get("count")}</button>
      </td>
      <td>
        ${this.props.factor * this.state.get("count")}
      </td>
    `.replaceAll("\n", "");
    return this.contents;
  }

}

Как в Реакте:

  • static HTMLTagWrapper: HTML-тег, отвечающий за перенос возврата this.renderHTML; его значение по умолчанию — div, и это статический атрибут, поэтому мы можем обратить на него внимание, не создавая экземпляр объекта только для этого.
  • ключ: необходимо предоставить — в React только с разными экземплярами одного и того же компонента, являющимися родственными элементами в дереве DOM;
  • конструктор: доступ к свойствам, определение начального состояния;
  • методы класса: обработчики событий, обращающиеся к переменным состояния

Обработчики событий передаются сразу после каждого рендеринга, устанавливая атрибут с именем event-{{тип события}} и передавая строку в формате {{идентификатор компонента/ключ события}} как ценность. Если событие установлено в HTML компонента, мы ДОЛЖНЫ определить метод с именем handle{{Tag}}{{Event type}}, как и в случае с camel. правило. Обработчику будет присвоен ключ в качестве параметра, поэтому мы можем правильно работать со сценариями, в которых у нас есть более одного элемента, запускающего события с одинаковым тегом и типом.

Отрисовка содержимого

Во-первых, мне нужно прояснить мою тривиальную стратегию выполнения рендеринга: шаблонные строки. Вызов компонента с именем «Родитель» означает добавление самозакрывающегося тега с именем component.parent. Каждый компонент, дважды вызываемый одним и тем же родителем, должен иметь атрибут key, чтобы различать их. Все остальные атрибуты будут рассматриваться как свойства.

setHTML = () => {
  const rows = this.state.get("row");
  const rowsHTML = rows.map((child, index) => {
    return `
      <component.parent
        key=${index}
        name="${child.name}"
        factor=${child.factor}
      />
    `;
  }).join("\n");
  
  return;
}

После первого шага обработки строки, на котором мы заменяем ‹component.* /› тегом-оболочкой (по умолчанию: div), получаем следующую строку ниже. Позже я планирую поддерживать функцию, подобную props.children. Атрибуту id присваивается значение UUID4 внутри конструктора компонентов.

`
  <div
    id=${this.id}
    key=${index}
    name="${child.name}"
    factor=${child.factor}
  ></div>
`

Важно отметить, что самозакрывающиеся/незакрывающиеся теги могут быть выданы в зависимости от используемого вами парсера, поэтому я решил извлечь имя компонента и атрибуты с помощью regex и применить парсер после преобразования в нативные HTML-теги.

И т. д

Здесь в основном есть два типа компонентов: root и non-root. «Корневой тип» получает обратный вызов рендеринга из библиотеки.

class Root extends Lib.Component(Lib.render, true) {}

в то время как «не-корневой» тип (все остальные компоненты) получает обратный вызов рендеринга «корневого типа»

const Main = new Root("root"); // instantiate Root

const Component = Lib.Component(Main.rootRender); // create alias for non-root

class Parent extends Component {}

Все это имеет смысл, потому что я применил заводской шаблон в классе, который возвращает анонимный класс;

const InternalComponent = (render = null, isRoot = false) => class {}

Возможно, мои следующие шаги, связанные с этой библиотекой:

  • повторно визуализировать только тот HTML, на который непосредственно влияет обновленное состояние
  • относится к шагу выше: обрабатывать изменения в свойствах компонентов
  • мощный сборщик мусора для отслеживания привязки элемента события — что-то вроде useEffect React, возвращающего обратный вызов, отвечающий за удаление прослушивателей событий
  • "Машинопись"
  • Библиотеки CSS — на данный момент в этом не было необходимости, так как мы можем использовать встроенный атрибут style
  • разделение кода — хорошая возможность узнать больше о вебпаке
  • тестирование — я написал шаблоны регулярных выражений с нуля, но я даже не помню, в каких случаях они выполняются или нет.