Приветствую Вас, дорогие читатели Хабр! В данной статье мы рассмотрим работы DOM на таких библиотеках JavaScript, как Vue, React, Angular. Материал поможет понять принцип работы, конечно, самый лучший способ разобраться в той или иной теме — это практика. В этой статье будут приведены несколько примеров для лучшего понимания материала.

DOM
DOM

Real DOM vs Shadow DOM vs Virtual DOM

Перед сравнением работы DOM на определенной библиотеке. Давайте рассмотрим, какие DOM есть, чем они отличаются, рассмотрим примеры.

1. Real DOM, или проще говоря DOM, обозначает объектную модель документа, и в это время представление в памяти нашего HTML в виде объектов. У нас есть несколько элементов нашего HTML, представленных в виде вложенных узлов, показанных в форме дерева. На картинке можете увидеть наглядный пример:

Real DOM
Real DOM

Для управления поведением узла нашего дерева мы используем querySelector. document.querySelector получает элемент из дерева и далее с помощью атрибутов мы можем работать над поведением.

Real DOM (управление с помощью querySelector)
Real DOM (управление с помощью querySelector)

2. Shadow DOM – это относительно новая функция DOM, которая позволяет вам создавать собственные компоненты. Например, кнопки воспроизведения и паузы, которые мы видим на странице, отсутствуют в Real DOM.

Shadow DOM
Shadow DOM

Видео-компонент – это пользовательский компонент, который был создан Chrome с использованием Shadow DOM. Chrome не отображает Shadow DOM по умолчанию, но мы можем включить в настройках. После включения в узле-родителе video мы видим дочерние узлы.

Все js-скрипты в данном примере скрыты от пользователя. Chrome делает работы за нас. Еще одно преимущество Shadow DOM заключается в том, что CSS-стили не влияют на дочерние узлы пользовательского компонента, который предоставляет Chrome. Такие пользовательские компоненты мы тоже можем написать.

3. Virtual DOM – абстракция поверх существующего DOM. То есть, у нас при изменении конкретных элементов дерева обновляется не всё дерево, а только измененные нами элементы. Проще говоря, идет сравнение между Real DOM и Virtual DOM. Такой процесс называется согласованием.

Сравнение принципа работы на библиотеках JavaScript

React использует паттерн проектирования "Наблюдатель" (observer). При изменении состояния компонента, React обновляет VDOM. После обновления VDOM, React сравнивает его текущую версию с предыдущей. Этот процесс называется "поиском различий" (diffing).

React и Vue примерно одинаково работают, они используют Virtual DOM. Но есть несколько отличий. По сравнению с React — Vue, напротив, использует алгоритм "обновления на месте" (in-place patching), который обновляет виртуальное DOM на месте без полного сравнения со старым состоянием.

React использует систему "подъема состояния" (state lifting) и "контекст" (context) для отслеживания изменений, тогда как Vue предоставляет систему наблюдателей (observers) и реактивных свойств (reactive properties). Vue также предлагает вычисляемые свойства (computed properties) и наблюдаемые свойства (watchers) для более удобной работы с реактивностью.

Пример реализации на Vue:

Родительский компонент
<template>
  <div>
    <child-component :count="count" @increment="incrementCount" />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      count: 0
    };
  },
  methods: {
    incrementCount() {
      this.count++;
    }
  }
};
</script>

Дочерний компонент
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="$emit('increment')">Increment</button>
  </div>
</template>

<script>
export default {
  props: ['count']
};
</script>

Пример реализации на React:

Родительский компонент
import { useState, createContext } from 'react';
import ChildComponent from './ChildComponent';

const MyContext = createContext();

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <MyContext.Provider value={{ count, setCount }}>
        <ChildComponent />
      </MyContext.Provider>
    </div>
  );
};

export default ParentComponent;

Дочерний компонент
import { useContext } from 'react';
import MyContext from './MyContext';

const ChildComponent = () => {
  const { count, setCount } = useContext(MyContext);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default ChildComponent;

Angular, в отличие от Vue и React, использует Incremental DOM. Incremental DOM используется компанией Google для внутренних нужд. Его основная идея такова: "Каждый компонент компилируется в набор инструкций, которые создают DOM-деревья и непосредственно обновляют их при изменении данных".

Например, код компонента, который мы видим ниже:

Компонент
@Component({
  selector: 'todos-cmp',
  template: `
    <div *ngFor="let t of todos|async">
        {{t.description}}
    </div>
  `
})
class TodosComponent {
  todos: Observable<Todo[]> = this.store.pipe(select('todos'));
  constructor(private store: Store<AppState>) {}
}

Будет скомпилирован в:

Скомпилированный компонент
var TodosComponent = /** @class */ (function () {
  function TodosComponent(store) {
    this.store = store;
    this.todos = this.store.pipe(select('todos'));
  }

  TodosComponent.ngComponentDef = defineComponent({
    type: TodosComponent,
    selectors: [["todos-cmp"]],
    factory: function TodosComponent_Factory(t) {
      return new (t || TodosComponent)(directiveInject(Store));
    },
    consts: 2,
    vars: 3,
    template: function TodosComponent_Template(rf, ctx) {
      if (rf & 1) { // create dom
        pipe(1, "async");
        template(0, TodosComponent_div_Template_0, 2, 1, null, _c0);
      } if (rf & 2) { // update dom
        elementProperty(0, "ngForOf", bind(pipeBind1(1, 1, ctx.todos)));
      }
    },
    encapsulation: 2
  });

  return TodosComponent;
}());

Данный пример взял из этой статьи.

Инкрементальный DOM в Angular, по сравнению с Vue и React, напротив, использует более прямой подход, обновляя только те части реального DOM, которые фактически изменились, без необходимости сравнивать целые деревья виртуального DOM.

Инкрементальный DOM в Angular часто считается более эффективным с точки зрения производительности, поскольку он применяет изменения напрямую к реальному DOM без дополнительных вычислительных затрат на сравнение деревьев виртуального DOM

Компания Google и поставила перед собой задачу — сделать так, чтобы приложения показывали хорошую производительность на мобильных устройствах. А значит, была необходима оптимизация размера бандла и объёма потребляемой памяти.

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

Комментарии (4)


  1. Daniel217D
    16.04.2024 20:55

    Было бы интересно посмотреть на примере внешне одинаковой страницы с большим количеством элементов сравнение тройки фреймворков. Как быстро выполняется первый рендеринг и обновление страницы. Но хз насколько страница должна быть большой, чтобы разница стала заметна

    Я выбрал самый простой фреймворк для запуска, для меня это Vue)


    1. glebanya12 Автор
      16.04.2024 20:55

      Постараюсь подробно рассмотреть это. Сам выбрал Vue, так как по синтаксису понравился; по моему мнению, Vue проще в освоении и более понятная библиотека.


    1. jodaka
      16.04.2024 20:55

      подробные бенчмарки есть для большинства сколь-нибудь известных js фреймворков https://krausest.github.io/js-framework-benchmark/2024/table_chrome_123.0.6312.59.html


      1. glebanya12 Автор
        16.04.2024 20:55

        Спасибо за информацию.