Learning Book

Observer API: Intersection, Resize, Mutation

IntersectionObserver и ResizeObserver

IntersectionObserver — видимость элементов

IntersectionObserver отслеживает пересечение элементов с viewport или другим элементом:

// Lazy loading изображений
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src  // загружаем реальное изображение
      img.removeAttribute('data-src')
      observer.unobserve(img)    // перестаём наблюдать после загрузки
    }
  })
}, {
  rootMargin: '50px'  // начать загрузку за 50px до появления в viewport
})

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img)
})

Параметры IntersectionObserver

const observer = new IntersectionObserver(callback, {
  root: null,           // null = viewport, или конкретный элемент
  rootMargin: '0px',   // отступ от root (как padding CSS)
  threshold: 0.5,      // 0..1 — какая часть должна быть видна
  // threshold: [0, 0.25, 0.5, 0.75, 1] — несколько порогов
})

// IntersectionObserverEntry
// entry.isIntersecting   — виден ли элемент
// entry.intersectionRatio — какая доля видна (0..1)
// entry.boundingClientRect — прямоугольник элемента
// entry.rootBounds       — прямоугольник root
// entry.target           — наблюдаемый элемент
IntersectionObserver гораздо эффективнее getBoundingClientRect() в scroll обработчике. Он работает на другом потоке и не вызывает forced reflow.

Анимации при появлении

const animateObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate-in')
    } else {
      entry.target.classList.remove('animate-in')
    }
  })
}, {
  threshold: 0.1  // начать анимацию когда 10% элемента видно
})

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  animateObserver.observe(el)
})

ResizeObserver — изменение размеров

const resizeObserver = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const { width, height } = entry.contentRect

    console.log(`Элемент изменил размер: ${width}x${height}`)

    // Responsive компоненты
    if (width < 600) {
      entry.target.classList.add('compact')
      entry.target.classList.remove('expanded')
    } else {
      entry.target.classList.add('expanded')
      entry.target.classList.remove('compact')
    }
  })
})

// Наблюдаем за изменениями размера
resizeObserver.observe(document.querySelector('.container'))

// Прекращаем наблюдение
resizeObserver.unobserve(element)
resizeObserver.disconnect()  // прекратить все наблюдения
const mutationObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('Добавлено:', mutation.addedNodes)
      console.log('Удалено:', mutation.removedNodes)
    }
    if (mutation.type === 'attributes') {
      console.log(<code>Атрибут ${mutation.attributeName} изменён</code>)
    }
  })
})

mutationObserver.observe(targetElement, {
  childList: true,        // наблюдать добавление/удаление детей
  subtree: true,          // наблюдать всё поддерево
  attributes: true,       // наблюдать изменения атрибутов
  characterData: true,    // наблюдать изменения текста
})