Skip to content
On this page

如何检测一个元素是否在视窗中

判断一个元素是否出现在视窗中有很多应用,比如

  • 懒加载图片:等到图片的位置进入视窗时,再去加载图片,可提高页面性能,减轻服务器压力;

  • 检测广告的曝光情况:为了计算广告收益,需要知道广告元素的曝光情况;

  • 赖加载其他资源等。

如何判断元素是否在视窗中就极为关键。

getBoundingClientRect 获取元素相对于视窗的位置

getBoundingClientRect可获取到元素尺寸和相对于视窗的为位置。

e6c9d24ely1h6i3cut37nj20hj0ceq39

js
{
  x: 100 // 元素左边框到视窗左上角的距离
  y: 182 // 元素上边框到视窗左上角的距离
  width: 300 // 元素宽度
  height: 250
  top: 182 // 元素上边框到视窗左上角的距离
  right: 400
  bottom: 432
  left: 100 // 元素左边框到视窗左上角的距离
}

如何获取视窗尺寸?

window.innerHeightwindow.innerWidth可用于获取视窗尺寸。

老的浏览器可使用document.documentElement.clientWidth获取。

js
function windowSize() {
  const viewportWidth = window.innerWidth || document.documentElement.clientWidth
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight
  return { width: viewportWidth, height: viewportHeight }
}

如何判断元素在视窗中?

竖直方向上,top < windowSize().height && top > -height, 在视窗中可见

水平方向上,left < widowSize().width && left > -width, 在视窗中可见

完全可见:top >= 0 && bottom <= windowSize().height && left >= 0 && right <= windowSize().width

js
function isElementVisible(element) {
  const _window = windowSize()
  const rect = element.getBoundingClientRect()
  return rect.top < _window.height && rect.top > -rect.height && rect.left < _window.width && rect.left > -rect.width
}

使用上述方法实现图片懒加载

页面上有非常多的图片,页面一打开就去加载所有图片,而用户未必需要马上看到,就造成页面卡顿和服务器压力过大,最好是当用户快要滚动到图片位置时,再去加载图片。

如何保存用户真正先看的图片呢? 放在自定义属性中。

<img src="https://fakeimg.pl/400x300" data-src="https://fakeimg.pl/400x300?text=hello" />

当 img 不在视窗中时,src 是一个小的预览图,当在视窗中时,使用 data-src 覆盖 src 实现加载新图片,从而实现图片懒加载。

html
<template>
  <div>
    <p>lazy load img</p>
    <ul>
      <li v-for="(i, index) in 10" :key="index">
        <img src="https://fakeimg.pl/400x300" data-src="https://fakeimg.pl/400x300?text=hello" />
      </li>
    </ul>
  </div>
</template>
<script>
  import { isElementVisible, debounce } from '@/utils'
  export default {
    name: 'LazyLoadImg',
    created() {
      const debounceLoadRealImg = debounce(this.loadRealImg, 200)
      window.addEventListener('scroll', debounceLoadRealImg)
      this.$once('hook:beforeDestroy', () => {
        window.removeEventListener('scroll', debounceLoadRealImg)
      })
    },
    mounted() {
      // NOTE 组件挂载后去加载可见区域的图片
      this.loadRealImg()
    },
    methods: {
      loadRealImg() {
        const imgList = document.querySelectorAll('img[data-src]')
        imgList.forEach(img => {
          const imgIsVisible = isElementVisible(img)
          if (imgIsVisible) {
            const realSrc = img.dataset.src
            img.src = realSrc
            delete img.dataset.src // 删除 dataset src,保证已经加载了真实图片的 img 不再加载
          }
        })
      },
    },
  }
</script>

<style scoped>
  ul > li {
    margin: 10px;
  }
</style>

scroll 时间时频繁触发并且值关心事件停止时的状态,使用防抖函数优化性能。

getBoundingClientRect 的缺点

主要是三个:

  • 需要做防抖或者节流处理,否则性能不佳;

  • 只能判断显示或者隐藏,难以做更小粒度控制,比如显示 img 显示 10% 再去加载图片;

  • 只能在判断在浏览器窗口中显示或者隐藏,在滚动的元素中判断就难以实现了。

使用交叉检测器接口

使用交叉检测器接口可避免上面三个缺点。

使用方式

js
// 观察的目标元素可见性变化时,执行 callback,options 是更细粒度的配置,可控制 callback 的执行频率
const io = new IntersectionObserver(callback, option)
// 开始观察
io.observe(document.getElementById('example'))

// 停止观察
io.unobserve(element)

// 关闭观察器
io.disconnect()

使用交叉检测器实现上述功能

当 img 可见时,加载图片

html
<script>
  export default {
    name: 'LazyLoadImg',
    mounted() {
      this.observeImg()
    },
    methods: {
      loadRealImg(item) {
        const img = item.target
        const realSrc = img.dataset.src
        // 隐藏或者已经加载了图片, 就不加载图片
        if (item.intersectionRatio <= 0 && !realSrc) return
        console.log(item.intersectionRatio)
        if (realSrc) {
          img.src = realSrc
          delete img.dataset.src // 删除 dataset src
        }
      },
      observeImg() {
        const io = new IntersectionObserver(entries => {
          entries.forEach(entry => {
            this.loadRealImg(entry)
          })
        })
        const imgList = document.querySelectorAll('img[data-src]')
        imgList.forEach(item => {
          io.observe(item)
        })
        this.$once('hook:beforeDestroy', () => {
          // 关闭观察器
          io.disconnect()
        })
      },
    },
  }
</script>

如何改进以上代码?

  1. 每个 img 加载完成后,停止观察;

  2. 使用一个标志记录已经加载了的 img 数量,等到都加载完毕,关闭观察器;

  3. 设置 img 加载错误处理;

  4. 设置粒度更加细的 options:

当 img 元素可见 80% 时,再加载图片,可这样设置:

  1. 添加过度样式等。

更多交叉检测器的 demo

See the Pen IntersectionObserver Visualizer by JackChouMine (@JackZhouMine) on CodePen.

图解交叉检测器


See the Pen Intersection Observer for Lazy Load Images by JackChouMine (@JackZhouMine) on CodePen.

懒加载图片

交叉检测器的其他应用

  • 导航栏吸顶

  • 拉下触底判断

  • div 尺寸检测

参考

Intersection Observer API

IntersectionObserver API 使用教程

使用 Intersection Observer API 构建无限滚动组件

Released under the MIT License.