前言
工作中经常通过判断元素是否进入视口,一般有三种方式进行判断
1. el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
2. el.getBoundingClientRect().top <= viewPortHeight
3. intersectionRatio > 0 && intersectionRatio <= 1 复制代码
根据元素与视口是否相交,可以进行吸顶、吸底、曝光上报、列表加载更多、图片懒加载等操作。
问题
前面两种需要通过监听scroll事件,为了防止频繁触发,需要做防抖处理。
用户体验
- 当元素进入视口时,总是需要延迟一定时间才能执行判断逻辑。
性能
- 在主线程上运行,因此频繁触发、调用会造成性能问题
- 无论是否触发相交,滚动结束后都会进行判断
- 获取srollTop的值和getBoundingClientRect方法都会导致回流
- 滚动事件会绑定多个事件处理函数,阻塞UI渲染
IntersectionObserver
当目标元素和根元素相交或失交,并且当前 tick 期间没有任务运行时,回调函数就会被执行
构造函数
let observer = new IntersectionObserver(callback, options); 复制代码
callback
每个entry描述一个观察到的目标元素的交集变化
let callback = (entries, observer) => {
entries.forEach(entry => { // entry.boundingClientRect // 目标元素的区域信息,getBoundingClientRect()的返回值 // entry.intersectionRatio // 目标元素的可见比例 // entry.intersectionRect // 目标元素与根元素交叉的区域信息 // entry.isIntersecting // 目标元素是否进入根元素区域 // entry.rootBounds // 根元素的矩形区域信息 // entry.target // 被观察dom节点 // entry.time // 相交发生时距离页面打开时的毫秒数 });
}; 复制代码
options
- root根元素,不指定默认为视窗
- rootMargin根元素的外边距
- threshold目标元素与根元素相交比例达到该值触发回调
实例方法
- observe开始监听一个目标元素(target),target必须是root的后代
- unobserve停止监听一个目标元素
- takeRecords返回所有监听的目标元素集合
- disconnect停止所有监听
参考
MDN Web Docs - Intersection Observer API
vue directive优化懒加载
步骤
防止直接加载图像,先把图片地址存放在data-src上
复制代码
LazyLoadDirective.js
export default { // hookFunction 当绑定元素插入父节点时调用 insert(el) { function loadImage() {
el.addEventListener('load', () => { // 加载完成后延迟添加class可以实现淡入动画 setTimeout(() => {
el.classList.add('loaded')
}, 100);
}); // 加载 data-url 的图片地址 el.src = el.dataset.url;
} function handleIntersect(entries, observer) {
entries.forEach(entry => { if (!entry.isIntersecting) { return;
} else { // 绑定元素进入视口后触发加载图片 loadImage(); // 并停止观察可见性变化, 防止再次加载图像。 observer.unobserve(el);
}
});
} function createObserver() { const options = { root: null, threshold: '0' }; const observer = new IntersectionObserver(handleIntersect, options); // 订阅观察当前绑定图片元素 observer.observe(el);
}
createObserver();
}
} 复制代码
思考
如果使用scroll事件来判断图片可见或不可见,每次需要重新计算的几十个图像,显然使用IntersectionObserver更加优雅。
注册指令
// main.js 全局注册 import Vue from 'vue'; import LazyLoadDirective from '@/directives/LazyLoadDirective';
Vue.directive('lazyload', LazyLoadDirective); 复制代码
小结
使用IntersectionObserver实现延迟加载非常简单,拥有很好的性能和用户体验,目前系统兼容性覆盖的比较广。当然,如果列表元素节点特别多时,也需要进行长列表优化。