Краткий ответ для тех, кто не хочет читать:
Функциональное программирование - истинная тема React. Виртуальная модель DOM - лишь одно из ее следствий, а не выгода. Он медленнее по определению, и DOM не может быть вообще, если вы используете React для звука или 3D-рендеринга. Выберите React, если он соответствует вашему мышлению, а не потому, что пару лет назад он был быстрее.

Начнем с противоположности виртуальной модели DOM: реальной модели DOM. Мы собираемся использовать несложный компонент Counter, HTML-код которого может выглядеть так:

<div>
 Count: 123
</div>
<div>
 <button type="button">Increment</button>
 <button type="button">Decrement</button>
<div>

Представляю, как бы вы его построили, используя простой JavaScript. Возможно, вы пойдете одним из двух способов: createElement или innerHTML.

Создание элементов вручную требует времени. Просто раздел кнопок почти равен высоте экрана:

class Counter {
  /* rest of the code */
  renderButton(text, handleClick) {
    const button = document.createElement("button");
    button.setAttribute("type", "button");
    button.textContent = text;
    button.addEventListener("click", handleClick);
    return button;
  }
  renderButtons() {
    const buttons = document.createElement("div");
 
    buttons.append(
      renderButton("Increment", this.handleIncrement),
      renderButton("Decrement", this.handleDecrement),
    );
  
    return buttons;
  }
}

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

innerHTML может выглядеть меньше, но для назначения слушателей требуются идентификаторы / классы:

class Counter {
  /* rest of the code */
  render() {
    this.container.innerHTML = `
      <div>
        Count: <span id="label">${this.count}</span>
      </div>
      <div>
        <button type="button" id="btn-inc">Increment</button>
        <button type="button" id="btn-dec">Decrement</button>
      <div> 
    `;
    this.label = document.getElementById("label");
    this.btnIncrement = document.getElementById("btn-inc");
    this.btnDecrement = document.getElementById("btn-dec");
    this.btnIncrement.addEventListener("click", this.handleIncrement);
    this.btnDecrement.addEventListener("click", this.handleDecrement);
  }
}

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

Конечно, никто не хочет делать такую ​​работу вручную. Вот почему у нас есть библиотеки пользовательского интерфейса, такие как Angular, Vue, Svelte и другие. Эти 2 варианта построения счетчика примерно соответствуют тому, что мы получаем в библиотеке на основе шаблонов.

innerHTML - это что-то вроде оригинального AngularJS: наш пакет содержит строку шаблона, а движок запускается на клиенте, анализируя этот шаблон, находя в нем слоты для данных и выражений, вставляя его на страницу и прикрепляя методы в качестве слушателей. Больший размер пакета и дополнительная нагрузка на браузер являются недостатками этого подхода.

createElement похож на современный Svelte / Ivy, где шаблон анализируется / компилируется во время сборки в набор команд управления документом, поэтому не требуется встраивание строк или время выполнения. Мы получаем меньше накладных расходов, а код оптимизирован специально для нашего компонента, но за счет потери функций на клиенте.

Выглядит не так уж сложно, правда?

Это потому, что мы забыли часть с языком шаблонов: условия и повторы. Все те хорошие вещи, без которых никто не может пользоваться шаблонами. Представьте, что вы добавили это к нашему Counter коду: вместо простого innerHTML нам нужно проанализировать строку и «запустить» динамические части. Что, если состояние изменится позже, как мы узнаем об этом? Будем ли мы повторно визуализировать только динамические части или весь компонент? Кодовая база будет сложнее и намного больше.

Но это еще не все. Что, если нам нужно использовать настраиваемый компонент Button?

<div
  component="Button"
  label="Increment"
  onclick="this.handleIncrement"
></div>

Это выполнимо. Просто создайте этот элемент div и передайте его как контейнер классу, зарегистрированному как Button. Но это необходимо заранее прописать:

const Button = require("../components/button.js");
UI.registerComponent("Button", Button);

Атрибуты следует анализировать, чтобы различать атрибуты HTML div и arguments Button. По сути, div теперь является поддеревом и должен работать сам по себе.

Но что, если мы хотим условно использовать не просто Button, а один из нескольких компонентов?

<div
  components="this.isLoading ? 'Button' : 'Image'"
  label="Increment"
  onclick="this.handleIncrement"
></div>

Это уже не простое сопоставление, а выражение, которое необходимо соответствующим образом скомпилировать с выполнением JS в нужное время и уничтожением / созданием экземпляров компонентов. И эти атрибуты могут каждый раз повторно анализироваться, потому что label может быть аргументом для Button, но не для Image.

Подумайте об исходном AngularJS со всеми его областями действия, иерархиями, включением и т. Д. Сложность сходит с ума с динамически вложенными шаблонами. Вот почему ng-include был статичным, и мы не могли просто отобразить любой шаблон на основе бизнес-логики.

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

Мы могли бы собрать команду суперзвезд и попытаться создать движок или компилятор, предоставляющий все эти функции, но дело в том, что почти каждая функция влияет на правила, по которым вы будете писать шаблон и / или логику, из-за ее сложности. И вы все еще несколько ограничены шаблоном.

Теперь давайте абстрагируемся и перейдем к области функциональных данных.

Все в мире может быть представлено в результате вызова функции и ее аргументов:

function(args) ⟶ anything

Внутри функции вы можете делать что угодно, включая вызов других функций (композицию). У нас и раньше были функции (методы) в классе Counter, но с другим внутренним содержанием.

Вместо того, чтобы производить только результат, методы изменяют существующее состояние (в нашем случае элементы документа с append или innerHTML), особенно при обновлении счетчика. В функциональном мире это запрещено, а переданные аргументы неизменяемы. Даже если мы передадим контейнер div в функцию, он не сможет добавить здесь узлы. Вместо этого мы должны полагаться только на возвращаемое значение. А в случае обновления повторно выполнить функцию и получить следующий результат.

Когда мы рисуем UI, возвращаемые значения должны как-то его описывать. Мы могли бы вернуть HTMLElement, но у него обязательно изменяемый интерфейс. В любом случае, как мы знаем, использование API документов вручную занимает много времени. Вернемся к HTML нашего компонента:

<div>
  Count: 123
</div>

Это не сильно отличается от объекта JavaScript.

const html = { element: "div", children: [
  "Count: 123"
] }

Обозначение объекта, безусловно, более подробное, поскольку общий язык должен быть для DSL. Но мы могли бы легко построить такие объекты сами, ничего не изменяя (и не разбирая шаблон). Мы могли бы даже сократить шаблон, реализовав небольшой помощник:

function element(name, …children) {
 return { element: name, children };
}
const ui = element("div",
  "Count: 123”
)

Более того, объекты могут ссылаться на функции, поэтому нам не нужна карта предварительно зарегистрированных компонентов:

function CounterLabel(children) {
  return element("div",
    "Count is ",
    element("span", ...children)
  );
}
const ui = element(CounterLabel, 0);

И результат будет:

const counterLabelResult = {
   element: "div",
   children: [
     "Count is ",
     { element: "span", children: [0] }
   ]
};
const ui = { element: CounterLabel, children: [0] };

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

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

function FancyButton(children, refresh) { … }

Предположим, что мы создали такую ​​функцию, которая рекурсивно обрабатывает дерево объектов, одновременно передавая обратный вызов. Назовем это getDescriber:

function getDescriber(component) {
  /*
  const describeUI = ...
  ...
  */
  return refresh => describeUI(component, refresh);
}
const describer = getDescriber(Counter);

describer принимает обратный вызов refresh и выводит полное описание пользовательского интерфейса в виде вложенного объекта строк, чисел и массивов (в основном, JSON).

Единственное, чего не хватает, - это функции для чтения этого описания и передачи элементов DOM в документ. Мы назовем его render и предположим, что у нас есть его реализация, уже кем-то сделанная:

function render(describer, mountNode) { ... }
render(describer, document.getElementById("root"));

Подведем итоги. У нас есть 2 части и всего 3 функции:

1. element(name, children) и getDescriber(component)
2. render(describer, mountNode)

Часть №1 состоит из element и getDescriber, используемых вместе для описания. Часть № 2 - это всего лишь render, которая используется исключительно тогда, когда вам нужно получить реальные элементы HTML. Обе части независимы. Единственное, что их связывает, - это структура описания. render ожидает вложенный объект со свойствами element и children. Это все.

Часть №1 может делать все, что угодно: генерировать функции / замыкания на лету и выполнять их, проверять условия любой сложности ... Вместо добавления еще одного сложного синтаксиса языка шаблонов вы просто используете всю мощь JavaScript. Пока он выводит требуемые объекты, никаких недостатков или ограничений у шаблонизаторов не существует.

Вы можете назвать это описание объекта virtual DOM, но только если вы используете эту конкретную render функцию, указанную выше. Мы можем сделать так, чтобы render вместо вызова document.createElement ... воспроизводил звуки! Мы можем интерпретировать описание как захотим. Это больше не DOM?

Как вы могли догадаться, Часть №1 - это react, а Часть №2 - это react-dom.

React - это не виртуальная модель DOM. Речь идет об абстрагировании физического тела ваших структурированных данных и помощи в обновлении этой структуры с течением времени. Вы работаете со структурой и данными с помощью React, кто-то другой материализует эту структуру позже. У веб-страниц действительно есть структура, поэтому React удобно иметь материализатор для DOM. Если бы Facebook был музыкальной компанией, возможно, React поставил бы вместо этого react-midi.

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