懒加载是一种前端常见优化性能技术,其核心思想是将页面中的资源延迟加载,即按需加载。这种方式有助于减少初始页面加载时间,减轻服务器和带宽的压力,从而加速页面加载速度
举个🌰 比方说当我们开发一个有很多图片的长网页时,一般先加载出现在视口内的几张图片,当滚动条滚动到相应图片的位置时再去加载别的图片,这种延迟加载的方式称之为 ~
大抵有俩种方式实现懒加载,一个是监听onscroll
属性,另外一个是文章的重点,IntersectionObserver
实现
监听onscroll属性实现
用前人的一张图来解释一下
- clientHeight:浏览器视口的高度
- scrollTop:滚动轴滚动的距离
- offsetTop:图片的头部距离浏览器顶部的高度(注意不是距离视口顶部的高度)
const imgs = document.querySelectorAll('img');
const lazyLoad = () =>{
let scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
let winTop = window.innerHeight;
for(let i=0 ;i < imgs.length; i++){
// 此时加载图片
if(imgs[i].offsetTop < scrollTop + winTop ){
imgs[i].src = imgs[i].getAttribute('data-src');
}
}
}
window.onscroll = lazyLoad
可以看到确实是已经实现了懒加载,但注意到控制台的打印,lazyLoad函数频繁触发,这个时候可以用节流
throttle
来优化一下
const throttle = (fn, delay) => {
let currentTime = Date.now()
return (...args) => {
nowTime = Date.now()
if (nowTime - currentTime > delay) {
fn(...args)
currentTime = Date.now()
}
}
}
感觉效果还行。。但是这种方法是有缺点的。由于scroll事件密集发生,计算量很大 在这样的背景下,IntersectionObserver登场
IntersectionObserver 操作
MDN的解释是
IntersectionObserver
接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)
- 图1 target元素刚与root元素交叉
- 图2 target元素与root元素交叉中
- 图3 target元素即将离开root元素,仍交叉
目标元素与root元素刚开始交叉和目标元素与root元素刚开始不交叉都能检测到
IntersectionObserver的执行时机
-
IntersectionObserver API
是异步的,不随着目标元素的滚动同步触发 -
IntersectionObserver
采用的是requestIdleCallback
(请求空闲回调),即只有线程空闲下来才会执行观察器。也就是说这个观察器的优先级非常低,只在其他任务执行完浏览器有了空闲才会执行
IntersectionObserver API
-
创建 IntersectionObserver 实例:
const observer = new IntersectionObserver(callback, options);
-
定义回调函数:回调函数会在目标元素与视口发生交叉时执行,
entries
是一个包含有关观察的目标元素的交叉信息的数组,observer
是IntersectionObserver
的实例function callback(entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { // 目标元素进入视口 } else { // 目标元素离开视口 } }); }
-
观察目标元素:
const target = document.querySelector('.目标元素'); observer.observe(target);
-
配置options:包含三个可选参数,
root
:dom根元素、rootMargin
:根元素的外边距,threshold
:阈值 详细可看MDNconst options = { root: document.querySelector('.根元素'), threshold: 0.5, // 交叉的阈值,0.5表示目标元素一半进入视口时触发回调, rootMargin: "10px 20px 30px 40px" // (top、right、bottom、left) };
-
处理回调函数:根据
entry.isIntersecting
属性的值确定目标元素是进入还是离开视口 -
停止观察:当不需要观察某个目标元素时
unobserve
方法来停止观察observer.unobserve(target)
-
断开观察器:当不需要使用
IntersectionObserver
时用disconnect
方法断开observer.disconnect()
注册的回调函数将会在主线程中被执行,所以该函数执行速度要尽可能的快,如果有一些耗时的操作需要执行还是使用Window.requestIdleCallback() 方法
IntersectionObserverEntry 对象
在浏览器随便找个dom元素打印一下entries
具体含义
- boundingClientRect:目标元素的矩形区域的信息
- intersectionRatio:表示目标元素的多少部分可见于视口,即
intersectionRect
占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0 - intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
- isIntersecting:目标元素与根元素是否相交
- isVisible:
Intersection Observer v2
引入的“可见性”的概念,若isVisible
为true,即目标元素完全不被其他内容遮挡,并且没有应用会改变或扭曲其在屏幕上的显示的视觉效果,若为false则不能保证可见性(一般不用) - rootBounds:根元素的矩形区域的信息,
getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null - target:被观察的目标元素,是一个 DOM 节点对象
- time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
实现懒加载
const imgs = document.querySelectorAll('[data-src]')
const observer = new IntersectionObserver(entries => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.src
observer.unobserve(item.target) // 停止观察当前元素 避免不可见时候再次调用callback函数
console.log(item.target.dataset.src +'图片已加载')
}
})
})
imgs.forEach(item => {
observer.observe(item)
})
// observer.disconnect() // 离开页面或者不需要observer时
看起来效果不错!
提问: intersectionRatio是否可以判断相交🤔
如果你滚动速度很快,当浏览器检测到相交时已经越过了 0 那个临界值,存在了实际的相交面积,entry.intersectionRatio > 0
, 此时等效于isIntersecting
为true。但如果滚动页面速度很慢,当目标元素的顶部和视口底部刚好挨上时浏览器检测到相交,回调函数触发,但此时 entry.intersectionRatio
等于 0 ,就不等效于isIntersecting
为true的时机了,继续向下滚回调函数也不会触发
详细可看代码片段