Для приложений с динамическим контентом важно автоматически
реагировать на добавление и удаление элементов с датами. Это достигается
с помощью MutationObserver — стандартного браузерного
API.
Стандартный вызов render(nodes) обрабатывает только
элементы, существующие на момент вызова. В приложениях с:
— элементы появляются и исчезают после инициализации. Без отслеживания новые элементы не получат автообновление.
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 меняется без пересоздания элемента —
например, при обновлении поста:
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 редко нужен — компоненты
управляют жизненным циклом через useEffect. Однако при
работе с порталами или внешними вставками может быть полезен.
// Остановить все обновления и наблюдение
observer.disconnect();
cancel(); // остановить все render-таймеры
MutationObserver с subtree: true может быть нагружен в
приложениях с частыми DOM-изменениями. Для оптимизации:
childList, не attributes,
если изменения атрибутов редки.