Отслеживание изменений в DOM

Для приложений с динамическим контентом важно автоматически реагировать на добавление и удаление элементов с датами. Это достигается с помощью MutationObserver — стандартного браузерного API.


Зачем отслеживать изменения DOM

Стандартный вызов render(nodes) обрабатывает только элементы, существующие на момент вызова. В приложениях с:

  • бесконечной прокруткой;
  • WebSocket-обновлениями;
  • динамическими формами и комментариями;
  • SPA с виртуальными DOM-переходами

— элементы появляются и исчезают после инициализации. Без отслеживания новые элементы не получат автообновление.


Базовое использование MutationObserver

import { render, cancel } from 'timeago.js';

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    // Обработка добавленных узлов
    mutation.addedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;

      const times = node.matches('[datetime]')
        ? [node]
        : Array.from(node.querySelectorAll('[datetime]'));

      if (times.length) render(times, 'ru');
    });

    // Обработка удалённых узлов
    mutation.removedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;

      if (node.matches('[datetime]')) cancel(node);
      node.querySelectorAll?.('[datetime]').forEach(cancel);
    });
  });
});

observer.observe(document.body, {
  childList: true,
  subtree:   true,
});

Ограниченное отслеживание (только конкретный контейнер)

const container = document.getElementById('feed');

observer.observe(container, {
  childList: true,
  subtree:   true,
});

Наблюдение за document.body может быть дорогостоящим для больших приложений. Лучше ограничить область конкретным контейнером.


Дебаунс для пакетных изменений

При массовом добавлении элементов (например, при загрузке страницы данных) MutationObserver может вызываться слишком часто:

import { render, cancel } from 'timeago.js';

let pendingNodes = new Set();
let debounceTimer = null;

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;

      const times = Array.from(node.querySelectorAll('[datetime]'));
      if (node.matches('[datetime]')) times.unshift(node);
      times.forEach(t => pendingNodes.add(t));
    });
  });

  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    if (pendingNodes.size) {
      render(Array.from(pendingNodes), 'ru');
      pendingNodes.clear();
    }
  }, 100); // пакетная обработка раз в 100ms
});

Отслеживание изменения атрибута datetime

Иногда datetime меняется без пересоздания элемента — например, при обновлении поста:

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'datetime') {
      const el = mutation.target;
      cancel(el);
      render(el, 'ru');
    }
  });
});

observer.observe(document.body, {
  attributes:    true,
  attributeFilter: ['datetime'],
  subtree:       true,
});

Автоматическое отключение при удалении

const managedElements = new WeakSet();

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;

      const times = node.querySelectorAll('[datetime]');
      times.forEach(t => {
        if (!managedElements.has(t)) {
          render(t, 'ru');
          managedElements.add(t);
        }
      });
    });

    mutation.removedNodes.forEach(node => {
      if (node.nodeType !== Node.ELEMENT_NODE) return;
      node.querySelectorAll?.('[datetime]').forEach(cancel);
    });
  });
});

React и MutationObserver

В React MutationObserver редко нужен — компоненты управляют жизненным циклом через useEffect. Однако при работе с порталами или внешними вставками может быть полезен.


Остановка наблюдателя

// Остановить все обновления и наблюдение
observer.disconnect();
cancel(); // остановить все render-таймеры

Производительность

MutationObserver с subtree: true может быть нагружен в приложениях с частыми DOM-изменениями. Для оптимизации:

  • Ограничить область наблюдения конкретным контейнером.
  • Использовать дебаунс для пакетных изменений.
  • Наблюдать только childList, не attributes, если изменения атрибутов редки.
  • Отключать наблюдатель, когда функция не нужна (например, на скрытых вкладках).