IntersectionObserver

1,779 阅读4分钟

intersectionobserverapi

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

MDN上的定义是: IntersectionObserver接口 (Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或者视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。

简单来说,IntersectionApi的功能就是用来判断:监听目标元素与其祖先或视窗交叉状态发生改变的手段

主要是用来检测 目标元素与root元素刚开始交叉目标元素与root元素刚开始不交叉

图示如下:

I3LtzC.png

IntersectionObserver API 是异步的, 不随着目标元素的滚动同步触发。即只有在线程空闲下来才会执行观察器。这意味着这个观察器的优先级非常的低,只有在其他的任务执行完,浏览器空闲了才会执行。

api基本使用

// callback 是当被监听元素的可见性变化时,触发的回调函数
// options是一个配置参数对象,可选的, 不进行配置时候存在对应的默认值
const observer = new IntersectionObserver(callback, options)

// IntersectionObserver接收的callback会在三种情况下被回调
// 1. 对应元素使用observe方法被添加到监听队列中
// 2. 对应元素和浏览器可视窗口刚开始产生交叉 --- 进入可视窗口
// 3. 对应元素和浏览器可视窗口由存在交叉转变为刚开始不交叉 --- 完全离开可视窗口
//  对元素target添加监听,当target元素变化时,就会触发回调
//  observe()的参数是一个DOM节点对象,如果要观察多个节点,就要多次调用这个方法
observer.observe(element);

// 移除一个监听,移除之后,target元素的交叉状态变化,将不再触发回调函数
observer.unobserve(element)

// 停止所有的监听
observer.disconnect();

IntersectionObserverEntry

// IntersectionObserverEntry对象提供目标元素的信息简化如下显示
{
  // time的值是一个时间戳,表示自观察器被实例化到被检测对象的交叉状态发生改变之间的时间戳
  // 例子:值为1000时,表示在IntersectionObserver实例化的1秒钟之后,目标元素的交叉状态发生改变了
  time: 78463997.025,
  
  // 根元素对应的矩形信息(即调用getBoundingClientRect()方法的返回值)
  // 如果没有根元素(即直接相对于视口滚动),则返回null
  rootBounds: null,
    
  // 目标元素的矩形信息
  boundingClientRect: DOMRectReadOnly { /* ... */ },
    
  // 目标元素与视口(或root根元素)的交叉区域的矩形信息
  intersectionRect: DOMRectReadOnly { /* ... */ },
    
  // 目标元素当前是否可见 Boolean值 可见为true
  isIntersecting: true,
    
  // 目标元素的可见比例 ---> [0, 1]
  intersectionRatio: 1,
    
  // 被监听的对象 --- 一个dom元素
  target: div#target.target
}

上面的矩形信息的关系如下:

I3P2Bt.png

option选项

IntersectionObserver构造函数的第二参数是一个配置对象, 他可以设置以下属性:

threshold

threshold属性 决定了什么时候触发回调函,值为一个数组,每一个数组项代表着一个门槛值

当目标元素和根元素相交的面积占目标元素面积的百分比到达或跨过这些指定的临界值时就会触发回调函数

threshold的默认值是[0, 1],即只有在开始进入,或者是完全离开视图区域时,才会触发回调函数

rootMargin

用来扩大或者缩小视窗的大小, 使用css的定义方式, 10px 10px 10px 20px 表示top,right,bottom, left的值。

root

root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点

简单示例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      height: 200vh;
      padding: 0 30px;
    }

    .target {
      width: 300px;
      height: 300px;
      margin-top: 2000px;
      background-color: red;
    }
  </style>
</head>
<body>
  <div id="target" class="target"></div>

  <script>
    // 创建IntersectionObserver实例对象 --- 参数是一个callback
    // entries是IntersectionObserverEntry对象数组
    // 监听了几个元素,数组长度就是多少
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        console.log(entry.isIntersecting ? 'div在可视区域内' : 'div不在可视区域内')
      })
    })

    // 添加对某个元素的监听
    observer.observe(document.getElementById('target'))
  </script>
</body>
</html>

应用场景

懒加载(lazy load)

我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。也就是所谓的”惰性加载”。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>lazy load</title>
  <style>
    body {
      height: 200vh;
      padding: 0 30px;
    }

    .target {
      width: 300px;
      height: 300px;
      margin-top: 2000px;
    }
  </style>
</head>
<body>
  <img
    id="target"
    class="target"
    data-src="https://s6.jpg.cm/2021/10/29/I3Pa3D.jpg"
    src="https://img.alicdn.com/tps/i3/T1QYOyXqRaXXaY1rfd-32-32.gif"
  />

  <script>
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.src = entry.target.getAttribute('data-src')
          entry.target.removeAttribute('data-src')
          observer.unobserve(entry.target)
        }
      })
    })

    observer.observe(document.getElementById('target'))
  </script>
</body>
</html>

无限加载(infinite scroll)

无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。

一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。

这样做的好处是,IntersectionObserver只要调用一次observe方法监听一个对象即可完成相应的功能

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>infinite scroll</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div id="container" class="container">
    <div id="loading">
      数据加载中 。。。
    </div>
  </div>

  <script src="./index.js"></script>
</body>
</html>

index.css

html,
body {
  height: 100%;
  font-size: 20px;
}

#loading {
  text-align: center;
}

.unit {
  height: 120px;
  border: 1px solid black;
  text-align: center;
}

.container {
  max-width: 300px;
  height: 500px;
  margin: 0px auto;
  overflow: auto;
  border: 5px solid #000;
}

index.js

let count = 0
const container = document.getElementById('container')

const observer = new IntersectionObserver(entries => {
  if (entries[0].isIntersecting) {
    const fragment = document.createDocumentFragment()

    for (let i = 0; i < 5; i++) {
      const dv = document.createElement('div')
      dv.className = 'unit'
      dv.innerHTML =  `第${count++ + 1}个元素`
      fragment.appendChild(dv)
    }

    container.insertBefore(fragment, entries[0].target)
  }
})

observer.observe(document.getElementById('loading'))

参考