Intersection Observer

Определяет пересечение элемента с его родителем или окном браузера.

Время чтения: 8 мин

Кратко

Скопировано

Intersection Observer — браузерный API, который позволяет асинхронно отслеживать пересечение элемента с его родителем или областью видимости документа (viewport). В момент пересечения можно запустить какое-либо действие, например, подгрузить дополнительные посты в ленте новостей («бесконечный скролл») или сделать «ленивую» загрузку контента.

Пример

Скопировано

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

Ещё одна фишка — изображение Морти немного увеличивается, когда полностью оказывается в наблюдаемой области. Такой трюк делается с помощью свойств threshold и intersectionRatio, о которых будет рассказано ниже.

Открыть демо в новой вкладке

Упрощённый код для этого примера выглядит приблизительно так.

        
          
          const lazyImages = document.querySelectorAll('.lazy-image')const callback = (entries, observer) => {  entries.forEach((entry) => {    if (entry.isIntersecting) {      console.log('Пользователь почти докрутил до картинки!')      entry.target.src = entry.target.dataset.src      observer.unobserve(entry.target)    }  })}const options = {  // root: по умолчанию window, но можно задать любой элемент-контейнер  rootMargin: '0px 0px 75px 0px',  threshold: 0,}const observer = new IntersectionObserver(callback, options)lazyImages.forEach((image) => observer.observe(image))
          const lazyImages = document.querySelectorAll('.lazy-image')

const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Пользователь почти докрутил до картинки!')

      entry.target.src = entry.target.dataset.src
      observer.unobserve(entry.target)
    }
  })
}

const options = {
  // root: по умолчанию window, но можно задать любой элемент-контейнер
  rootMargin: '0px 0px 75px 0px',
  threshold: 0,
}

const observer = new IntersectionObserver(callback, options)

lazyImages.forEach((image) => observer.observe(image))

        
        
          
        
      

Как пишется

Скопировано

Intersection Observer создаётся с помощью конструктора:

        
          
          const observer = new IntersectionObserver(callback, options)
          const observer = new IntersectionObserver(callback, options)

        
        
          
        
      

На вход принимает функцию-колбэк, которая будет выполняться при пересечении области и элементов, а также дополнительные настройки пересечения options.

Функция-колбэк

Скопировано

Колбэк принимает два аргумента:

1️⃣ entries — список объектов с информацией о пересечении. Для каждого наблюдаемого элемента создаётся один объект IntersectionObserverEntry.

Объект содержит несколько свойств, самые полезные это:

  • isIntersecting — булево значение. true если есть пересечение элемента и наблюдаемой области.
  • intersectionRatio — доля пересечения от 0 до 1. Если элемент полностью в наблюдаемой области, то значение будет 1, а если наполовину, то — 0.5.
  • target — сам наблюдаемый элемент для дальнейших манипуляций. Например, для добавления классов.

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

2️⃣ observer — ссылка на экземпляр наблюдателя для вызова методов прослушивания:

  • observe(элемент) — запускает наблюдение за переданным элементом;
  • unobserve(элемент) — убирает элемент из списка наблюдаемых;
  • disconnect() — останавливает наблюдения за всеми элементами.

options

Скопировано

Необязательный аргумент в виде объекта с тремя свойствами:

  • root — элемент, который будет областью наблюдения. Должен быть предком наблюдаемого элемента. По умолчанию — window.
  • rootMargin — строка с отступами для области наблюдения. Синтаксис почти такой же, как у CSS-свойства margin'0px 0px 0px 0px' (top, right, bottom, left), так же доступна короткая запись, например '50px 100px' или '200px'. По умолчанию — '0px 0px 0px 0px'. Если они установлены, то пересечение будет учитывать эти отступы.
Визуализация работы rootMargin
  • threshold — порог пересечения, при котором будет срабатывать колбэк. Может быть либо одним числом от 0 до 1, либо массивом значений, например [0, 0.5, 1]. По умолчанию — 0.
Подробнее о значениях threshold
  • 0 — сработает даже при пересечении на один пиксель;
  • 1 — сработает только при полном появлении элемента в наблюдаемой области;
  • [0, 0.5, 1] — колбэк сработает три раза: сначала при попадании хотя бы одного пикселя элемента в область наблюдения, затем на половине элемента и ещё один раз, когда элемент окажется полностью в области наблюдения.

Как понять

Скопировано

Intersection Observer называется так, потому что он реализует паттерн программирования «Наблюдатель». Наблюдатель следит за положением наблюдаемых элементов и выполняет действия при их пересечении с контейнером. Это работает аналогично подпискам на события через метод addEventListener().

Intersection Observer работает асинхронно и не блокирует основной поток. Это позволяет приложениям оставаться плавными и при этом творить магию, например, как на сайте Apple.

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

В коде ниже проверяется факт пересечения элемента и области наблюдения. Так как мы не знаем, какой элемент из нашего списка пересёк границу контейнера, то нужно найти его с помощью обхода entries. Если элементы пересекаются, то в консоль выводится сообщение, а затем наблюдение за этим элементом прекращается с помощью вызова метода unobserve(). Такое поведение подходит, например, для «ленивой» загрузки.

        
          
          const callback = (entries, observer) => {  entries.forEach((entry) => {    if (entry.isIntersecting) {      console.log('Элемент пересёк границу области и всё ещё соприкасается с ней!')      observer.unobserve(entry.target)    }  })}
          const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      console.log('Элемент пересёк границу области и всё ещё соприкасается с ней!')

      observer.unobserve(entry.target)
    }
  })
}

        
        
          
        
      

Другой случай — необходимо определить, какая часть элемента находится в области наблюдения. Для этого используем объект options:

        
          
          const options = {  root: document.querySelector('.container'),  rootMargin: '0px',  threshold: [0, 0.5, 1],}
          const options = {
  root: document.querySelector('.container'),
  rootMargin: '0px',
  threshold: [0, 0.5, 1],
}

        
        
          
        
      

В качестве наблюдаемой области возьмём элемент с классом container без каких-либо дополнительных отступов. Интерес здесь представляет свойство threshold. Оно означает, что необходимо вызвать колбэк в трёх случаях:

  • сначала элемент пересёк область хотя бы на 1 пиксель;
  • затем половина элемента оказалась в области;
  • и наконец элемент полностью внутри наблюдаемой области.

Остаётся узнать какое именно событие произошло. Для этого у IntersectionObserverEntry есть свойство intersectionRatio.

        
          
          const callback = (entries) => {  entries.forEach(({ isIntersecting, intersectionRatio }) => {    if (isIntersecting) {      if (intersectionRatio >= 0 && intersectionRatio < 0.45) {        console.log('Элемент появился в области наблюдения')      }      if (intersectionRatio >= 0.45 && intersectionRatio < 0.75) {        console.log('Элемент наполовину в области наблюдения')      }      if (intersectionRatio === 1) {        console.log('Элемент полностью в области наблюдения')      }    }  })}
          const callback = (entries) => {
  entries.forEach(({ isIntersecting, intersectionRatio }) => {
    if (isIntersecting) {
      if (intersectionRatio >= 0 && intersectionRatio < 0.45) {
        console.log('Элемент появился в области наблюдения')
      }

      if (intersectionRatio >= 0.45 && intersectionRatio < 0.75) {
        console.log('Элемент наполовину в области наблюдения')
      }

      if (intersectionRatio === 1) {
        console.log('Элемент полностью в области наблюдения')
      }
    }
  })
}

        
        
          
        
      

После определения колбэка и настроек создаём сам наблюдатель и запускаем прослушивание на элементе с классом element:

        
          
          const targetElement = document.querySelector('.element')const observer = new IntersectionObserver(callback, options)observer.observe(targetElement)
          const targetElement = document.querySelector('.element')
const observer = new IntersectionObserver(callback, options)

observer.observe(targetElement)

        
        
          
        
      

При необходимости можно прекратить наблюдение за всеми элементами или за каким-то одним:

        
          
          observer.disconnect()observer.unobserve(targetElement)
          observer.disconnect()

observer.unobserve(targetElement)

        
        
          
        
      

Применение

Скопировано

С помощью этого инструмента можно создавать бесконечные ленты как во многих популярных соц. сетях, запускать красивые анимации как на сайте Apple и даже создавать необычные странички как bertani.net.

Исторически обнаружение видимости отдельного элемента или видимости двух элементов по отношению друг к другу было непростой задачей. Варианты решения этой задачи были ненадёжными и замедляли работу браузера из-за работы в основном потоке. Intersection Observer API работает асинхронно, что несомненно улучшает производительность приложения.

Наглядные сравнения можно найти в статье Scroll listener vs Intersection Observers: a performance comparison