IntersectionObserver实现图片懒加载与无限滚动
背景
图片懒加载和滚动加载数据的需求一直存在,比较常见的方法是监听页面或者容器的滚动事件,实时计算节点与容器边界的关系,以实现不同的加载逻辑。通常会使用EventListener
来绑定监听事件,同时借助Element.getBoundingClientRect()
来获取相关元素的边界信息,然后两者都是在主线程运行,会占用一定的资源和性能,因此频繁触发事件和调用方法可能会造成性能问题,这种检测方法极其怪异且不优雅。Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport相交区域大小变化的方法,它会注册一个回调函数,当监听的两个元素的相交区域大小发生变化时(包括被监视的元素进入或者退出另外一个元素时),该回调方法会被触发执行。这样主线程将不必浪费资源监听滚动和实时计算,浏览器会自行优化元素相交管理。
Intersection Observer API简介
创建一个Intersection Observer实例并添加监听对象:
const el = documen.querySelector('#target') // 被监听的元素
const observer = new IntersectionObserver(callback, options) // 创建实例
observer.observe(el) // 开始监听元素
其中callback
为必须配置的回调函数,options
用于配置observer实例的监听环境,observe()
方法用于注册监听。MDN文档
Intersection observer options
如果没有指定options,observer实例会使用当前viewport作为root,没有margin,阈值为0,意味着即使一像素的改变都会触发回调函数。可配置的参数(节自MDN)如下:
root
指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null
,则默认为浏览器视窗
rootMargin
根(root)元素的外边距。类似于 CSS 中的 margin
属性,比如 "10px 20px 30px 40px"
(top, right, bottom, left)。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为0
threshold
可以是单一的 number 也可以是 number 数组,target 元素和 root 元素相交程度达到该值的时候 IntersectionObserver 注册的回调函数将会被执行。如果你只是想要探测当 target 元素的在 root 元素中的可见性超过50%的时候,你可以指定该属性值为0.5。如果你想要 target 元素在 root 元素的可见程度每多25%就执行一次回调,那么你可以指定一个数组 [0, 0.25, 0.5, 0.75, 1]
,默认值是0。该值为1.0含义是当 target 完全出现在 root 元素中时候 回调才会被执行。
Intersection observer callback
回调函数当以下情况发生时会被调用:
- Observer 第一次监听目标元素时(observe()方法执行时)
- 每当目标(target)元素与设备视窗或者其他指定元素(root)发生交集时(当元素可见比例超过指定阈值后)
回调函数调用时会接收两个参数:
entries
:一个IntersectionObserverEntry
对象的数组
observer
:被调用的IntersectionObserver
实例
IntersectionObserverEntry
IntersectionObserverEntry
描述了目标元素与其根元素容器在某一特定过渡时刻的交叉状态. IntersectionObserverEntry
的实例作为 entries
参数被传递到一个 IntersectionObserver
实例的回调函数中,它拥有以下只读属性:
boundingClientRect
:返回包含目标元素的边界信息的DOMRectReadOnly
. 边界的计算方式与 Element.getBoundingClientRect()
相同
intersectionRatio
:返回intersectionRect
与 boundingClientRect
的比例值,范围为0~1
intersectionRect
:返回一个 DOMRectReadOnly
用来描述根和目标元素的相交区域
isIntersecting
:返回一个布尔值,描述目标元素是否与根元素有相交区域
rootBounds
:返回一个 DOMRectReadOnly
用来描述交叉区域观察者(intersection observer)中的根元素
target
:与根出现相交区域改变的元素 (Element
)
time
:返回一个记录从 IntersectionObserver
的时间原点(time origin)到交叉被触发的时间的时间戳(DOMHighResTimeStamp
)
IntersectionObserver
IntersectionObserver拥有以下方法:
disconnect()
:停止实例的所有监听
observe()
:开始监听
takeRecords()
:返回所有观察目标的IntersectionObserverEntry
对象数组
unobserve()
:停止监听特定元素
Intersection Observer API实战
图片懒加载
在我们访问一个图片展示比较多的网页时,加载速度慢很多时候正是因为图片多导致,大量的img图片导致页面渲染的堵塞。当费了许多力气把全部图片和页面加载出来时而用户早已离去。另一方面,若用户只查看了网页的前面部分便离开,许多已经加载却因为处于网页底部而未呈现在视口区的图片,它们极大加重服务器压力了但是用户看都没看,白白浪费了性能。为了解决上面的问题需要引入图片懒加载,懒加载其实很好理解,重点就是一个‘懒'字。当用户滚动相应可视区域,若可视区域有图片便加载,而在可视区域外未加载过的图片它们先不加载,如果用户滚动可视区域到它们时它们再加载,否则一律不加载。这样一来就大大提高了网页渲染的性能和减少不必要的浪费。
有了Intersection Observer API之后,我们可以很容易实现图片的懒加载。此示例使用原生HTML+JavaScript实现,大致思路如下:
图片占位:此处我们使用HTML5的dataset属性,将真实的图片地址赋值给
data-src
属性创建Intersection Observer实例并配置回调函数
const observer = new IntersectionObserver(entries => { for (const i of entries) { if (i.isIntersecting) { // 当目标元素出现在视图内 const img = i.target; const trueSrc = img.getAttribute("data-src"); setTimeout(() => { img.setAttribute("src", trueSrc); // 方便展示懒加载效果 }, 1500); observer.unobserve(img); // 停止监听此元素 } } });
在初始化实例的时候可以使用箭头函数直接绑定回调函数,在回调函数内,我们设置好图片的真实地址后应该停止监听此图片,以避免不必要的处理。
监听所有img元素
const images = document.getElementsByTagName("img"); for (const i of images) { observer.observe(i); }
需要注意,设置监听应在页面结构渲染结束后,即脚本应在window.onload
时执行,因为在调用observer.observe()
时,回调函数会执行一次,此时元素未初始化,高度为0,if
语句内的脚本会执行。如果无法监听window.onload
事件,可以给元素设置基础的宽高,保证初始化脚本时只有部分元素在视图内。实际效果如下方gif图:
可以看到刷新页面后,首次只加载了出现在视图内的三至四张图片,随着页面的滚动,出现的img
标签src
属性被修改为真实的图片地址。
无限滚动
内容无限滚动其实就是当用户滚动到接近内容底部时主动加载新的数据,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉。这里提供两种实现思路,都是在Intersection Observer的回调函数内做相关的判断和操作,可以自行尝试实现:
- 存储图片信息列表,在回调函数内判断如果当前元素是列表的最后一项,则触发加载下一页的数据,新加载的数据与原来的列表合并即可,加载数据后要监听新附加的元素
- 页面加入页尾栏(又称sentinels),一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的数据放在页尾栏前面
下图为无限加载示例,使用Unsplash的API,每次会加载若干张随机的图片: