图片懒加载的三种解决方法
写作提高思考
前言
我想写一个系列,关于图片懒加载、React 渲染十万条数据、无限下拉方案文章,因为其三者有共性,都有使用了 与 DOM 相关的 offsetTop、innerHeight、getBoundingClientRect、IntersectionObserver 等,这些知识点如果单独放在一篇文章中,其价值点就是1,如果相互连接,价值点就是3。用好梅特卡夫定律,能让自己的效率提升不少
正文
学习前端,三板斧,HTML、CSS、JavaScript。JavaScript 基础由 DOM、BOM、ECMAScript 组成,其中 ECMAScript 为规范语言,现在说的 ES6(ES2016~ES2022) 指的就是它,隔段时间就会发布,目前一年发布一次,以年份来说,现在是 ECMAScript 2022。BOM是什么,BOM是浏览器对象模型(Browser Object Model)。它有六大对象
- document: DOM(对 BOM 包含了DOM,但是 DOM 重要,其地位和 BOM 一样)
- event:事件对象
- history:浏览器的历史记录
- location:窗口的 url 地址栏信息
- screen:显示设备的信息
- navigator:浏览器的配置信息
DOM我们也很了解,文本对象模型,指操作HTML(超级文本标识语言)的API。DOM 会将文档解析为一个由节点和对象(包含属性和方法的对象)组件的结构集合
以前开发页面时,我们在 script 标签中,先获取节点(DOM Api),再操作 DOM ,所以以前是 《JavaScript 面向对象编程》,《JavaScript dom编程艺术》,但操作 DOM 的 API 太长,不易书写,JQuery 集大成,简化Api,统一了操作写法。如果展开,会有很多可以延伸,而我铺垫了这么多,就是想引出 document
这次我们要讲的 offset、scroll、client 就是出自“ document 家”。先配两张图来看看这三个到底是什么
client
client 指元素本身的可视内容。不包括 overflow 被折叠部分,不包括滚动条、border,包括 padding
有四属性:
- clientHeight:对象可见的高度
- clientWidth:对象可见宽度
- clientTop:元素距离顶部的厚度,一般为0,因为滚动条不会出现在顶部
- clientLeft:元素距离左侧的厚度,一般为0,因为滚动条不会出现在左侧
offset
offset 指偏移。包括这个元素在文档中占用的所有显示宽度,包括滚动条、padding、border,不包括 overflow 隐藏的部分
有五属性:
offsetHeight:该对象自身的绝对高度
- offsetHeight: = border-width * 2 + padding-top + height + padding-bottom
offsetWidth:该对象自身的绝对宽度
- offsetWidth = border-width * 2 + padding-left + width + padding-right
offsetParent:返回一个对象的引用,字面意思,相对父元素的偏移
- 如果当前元素的父元素没有 CSS 定位(position为absolute/relative),offsetParent 为 body
- 如果当前元素的父元素有 CSS 定位(position为absolute/relative),offsetParent 取父级中最近的元素
offsetTop:相对版面或 offsetParent 属性指定父坐标的顶部距离
- offsetTop = offsetParent 的 padding-top + 中间元素的 offsetHeight + 当前元素的 margin-top
offsetLeft:相对版面或 offsetParent 属性指定父坐标的左部距离
- offsetLeft = offsetParent 的 padding-left + 中间元素的 offsetWidth + 当前元素的 margin-left
Scroll
Scroll 指滚动。包括这个元素没有显示出来的实际宽度,包括 padding,不包括滚动条、border
scrollHeight:获取对象的滚动高度,对象的实际高度
scrollWidth:获取对象的滚动宽度
scrollTop:当前元素与窗口最顶端的距离
scrollLeft:当前元素与窗口最左端的距离
其他
innerHeight 和 clientHeight 有什么区别
准确来说,clientHeight 是针对 body,innerHeight 是 window 的
document.body.clientHeight:网页可见区域高
window.innerHeight:可视窗口高度,不包括浏览器顶部工具栏
监听图片高度实现懒加载
通过图片的 offsetTop(偏移高度)和 window 的 innerHeight、scrollTop 判断图片是否位于可视区域
即很多图片,先显示视窗中的图片,没看见的先不展示,加快页面加载速度。当你向下滚,当后续图片的 offsetTop(偏移高度) 小于 innerHeight(视窗高度) + scrollTop(滚动高度) 时,意味着此图片已经出现在视窗中,将真正图片替换loading
关键代码在于
function lazyload() {
let seeHeight = window.innerHeight;
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for (let i = n; i < img.length; i++) {
if (img[i].offsetTop < seeHeight + scrollTop) {
// 对比图片的偏移高度和屏幕高度+滚动高度
if (img[i].getAttribute("src") === "loading.gif") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
效果如下:
Element.getBoundingClientRect
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置
getBoundingClientRect 返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,即与该元素相关的 CSS 边框集合
语法
domRect = element.getBoundingClientRect();
返回坐标、宽高、在视口中的位置
- x
- y
- width
- height
- top
- right
- bottom
- left
如果是标准盒子模型,元素的尺寸等于
width/height
+padding
+border-width
的总和。如果box-sizing:border-box
,元素的尺寸等于width/height
我们用这个 API 来获取每张图片的 top 值,如果 top 值小于可视区的高度就视为已经进入可视区,直接加载图片即可
function lazyload() {
let seeHeight = document.documentElement.clientHeight;
for (let i = n; i < img.length; i++) {
if (img[i].getBoundingClientRect().top < seeHeight) {
if (img[i].getAttribute("src") === "loading.gif") {
img[i].src = img[i].getAttribute("data-src");
}
n = i + 1;
}
}
}
效果如下:
通过 IntersectionObserver 实现懒加载
IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)
IntersectionObserver 可以不用监听 scroll 事件,做到元素一可见便调用回调,在回调里面我们来判断元素是否可见
if (IntersectionObserver) {
let lazyImageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry, index) => {
let lazyImage = entry.target;
// 如果元素可见
if (entry.intersectionRatio > 0) {
if (lazyImage.getAttribute("src") === "loading.gif") {
lazyImage.src = lazyImage.getAttribute("data-src");
}
lazyImageObserver.unobserve(lazyImage);
}
});
});
for (let i = 0; i < img.length; i++) {
lazyImageObserver.observe(img[i]);
}
}
上述代码表示,遍历所有的图片,对其进行观察原生是否可见,如果元素可见,就把真正图片替换loading
IntersectionObserver 可以自动“观察”元素是否可见,其本质是目标元素与视窗产生一个交叉去,所以这个 API 叫做“交叉观察器”
使用方式
let io = new IntersectionObserver(callback, option);
上面代码中, IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)
构造函数的返回值是一个观察器实例。实例的 observe
方法可以指定观察哪个 DOM 节点
// 开始观察
io.observe(document.getElementById("example"));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数 callback
callback
一般会触发两次。一次时目标元素刚刚进入视窗(开始可见),另一次时完全离开视窗(开始不可见)
let io = new IntersectionObserver((entries) => {
console.log(entries);
});
上面代码中,回调函数采用的是箭头函数的写法。callback
函数的参数(entries
)是一个数组,每个成员都是一个 IntersectionObserverEntry
对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
IntersectionObserverEntry 对象
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性
- time: 可见性发生变化的时间,单位毫秒
- target:被观察的目标,是个 DOM 节点对象
- rootBounds:根元素的矩形区域的信息,getBoundingClientRect() 方法的返回值,如果没有根元素(即直接相对于视窗滚动),则返回null
- boundingClientRect:目标元素的矩形区域的信息
- intersectionRect:目标元素与视窗(或根元素)的交叉区域的信息
- intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为1,完全不可见时小于等于 0
如图所示:
兼容性如何
caniuse 兼容性报告目前支持率是 93.67%,但是iOS的支持度要在 iOS12.2 以上,如果是iPhoneX(2018.11)之后的手机都是支持的,如果是之前的,升级系统才支持,考虑到一些人是不会升级,所以这个兼容性还不支持大众化的场景,但它的能力和性能都非常的好
总结
面试的时候被问到懒加载,我那个时候没做过相关的准备,我说不知道,途虎的面试官会引导,其实引导才能测试出一个人真正的水平,但是那个时候我竟然连 scroll 都想不起来。现在回想起来,实在是准备的方向搞错了。
说到图片懒加载,有两种方法:
- 监听图片高度
- 技术要点:监听scroll,滚动的时候遍历所有的图片,如果图片的偏移高度小于屏幕高度+滑动高度,说明已经出现在视窗,就替换图片
- 优点:兼容性好
- 缺点:单纯使用 scroll 滑动来监听高度,会引发性能问题,所以要搭配节流
- Element.getBoundingClientRect
- 技术要点:与监听图片无太大区别,无非视把图片的偏移高度改成 getBoundingClientRect().top,对比每张图片的自身高度是否出现在视窗(视口)中,有就替换图片
- 优点:兼容性好,代码相对监听图片高度少了一些
- 缺点:也是使用 scroll 滑动来监听,会引发性能问题
- 使用 IntersectionObserver Api
- 技术要点:通过 IntersectionObserver Api 来实现,图片元素一可见就调用回调,在回调中判断元素是否可见
- 优点:写起来方便,性能好
- 缺点:兼容性适配iOS12.2以上,安卓5以上
附上线上 demo: