Клавиша / esc

Intersection Observer

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

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

Кратко

Скопировано

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 с информацией о пересечении.

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

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

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

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

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

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

options

Скопировано

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

delay — задержка перед срабатыванием наблюдателя (в миллисекундах). Значение по умолчанию — 0 мс. Если trackVisibility равен true, то значение delay не может быть меньше 100 мс и будет автоматически увеличено при необходимости.

root — элемент, который будет областью наблюдения. Должен быть предком наблюдаемого элемента. По умолчанию — window.

rootMargin — строка с отступами для области наблюдения. Синтаксис почти такой же, как у CSS-свойства margin'0px 0px 0px 0px' (top, right, bottom, left), так же доступна короткая запись, например, '50px 100px' или '200px'. По умолчанию — '0px 0px 0px 0px'. Если они установлены, то пересечение будет учитывать эти отступы.

scrollMargin — строка с отступами, определяющая смещения, добавляемые к контейнеру прокрутки при вычислении пересечений. Синтаксис почти такой же, как у 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]` — колбэк сработает три раза: сначала при попадании хотя бы одного пикселя элемента в область наблюдения, затем на половине элемента и ещё один раз, когда элемент окажется полностью в области наблюдения.

trackVisibility — логическое значение, указывающее, следует ли наблюдателю отслеживать видимость целевого элемента. Если значение равно true, браузер будет учитывать реальную видимость целевого элемента. Например, если элемент перекрыт попапом, колбэк не будет вызван при пересечении с наблюдаемой областью.
Отслеживание видимости требует дополнительных ресурсов, поэтому его следует использовать только при необходимости.
Значение по умолчанию — false. Пересечение определяется только по геометрии.

Как понять

Скопировано

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.