如何检测一个元素是否在视窗中
判断一个元素是否出现在视窗中有很多应用,比如
懒加载图片:等到图片的位置进入视窗时,再去加载图片,可提高页面性能,减轻服务器压力;
检测广告的曝光情况:为了计算广告收益,需要知道广告元素的曝光情况;
赖加载其他资源等。
如何判断元素是否在视窗中就极为关键。
getBoundingClientRect 获取元素相对于视窗的位置
getBoundingClientRect
可获取到元素尺寸和相对于视窗的为位置。
{
x: 100 // 元素左边框到视窗左上角的距离
y: 182 // 元素上边框到视窗左上角的距离
width: 300 // 元素宽度
height: 250
top: 182 // 元素上边框到视窗左上角的距离
right: 400
bottom: 432
left: 100 // 元素左边框到视窗左上角的距离
}
如何获取视窗尺寸?
window.innerHeight
和window.innerWidth
可用于获取视窗尺寸。
老的浏览器可使用document.documentElement.clientWidth
获取。
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
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 实现加载新图片,从而实现图片懒加载。
<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% 再去加载图片;
只能在判断在浏览器窗口中显示或者隐藏,在滚动的元素中判断就难以实现了。
使用交叉检测器接口
使用交叉检测器接口可避免上面三个缺点。
使用方式
// 观察的目标元素可见性变化时,执行 callback,options 是更细粒度的配置,可控制 callback 的执行频率
const io = new IntersectionObserver(callback, option)
// 开始观察
io.observe(document.getElementById('example'))
// 停止观察
io.unobserve(element)
// 关闭观察器
io.disconnect()
使用交叉检测器实现上述功能
当 img 可见时,加载图片
<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>
如何改进以上代码?
每个 img 加载完成后,停止观察;
使用一个标志记录已经加载了的 img 数量,等到都加载完毕,关闭观察器;
设置 img 加载错误处理;
设置粒度更加细的 options:
当 img 元素可见 80% 时,再加载图片,可这样设置:
- 添加过度样式等。
更多交叉检测器的 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 尺寸检测