图片懒加载的三种解决方法

写作提高思考

前言

我想写一个系列,关于图片懒加载、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 家”。先配两张图来看看这三个到底是什么

document

document2

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

rect

如果是标准盒子模型,元素的尺寸等于 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;
    }
  }
}

效果如下:

getBoundingClientRect实现懒加载

通过 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

如图所示:

IntersectionObserverEntry参数

兼容性如何

caniuse 兼容性报告目前支持率是 93.67%,但是iOS的支持度要在 iOS12.2 以上,如果是iPhoneX(2018.11)之后的手机都是支持的,如果是之前的,升级系统才支持,考虑到一些人是不会升级,所以这个兼容性还不支持大众化的场景,但它的能力和性能都非常的好

image-20220125104012150

总结

面试的时候被问到懒加载,我那个时候没做过相关的准备,我说不知道,途虎的面试官会引导,其实引导才能测试出一个人真正的水平,但是那个时候我竟然连 scroll 都想不起来。现在回想起来,实在是准备的方向搞错了。

说到图片懒加载,有两种方法:

  • 监听图片高度
    • 技术要点:监听scroll,滚动的时候遍历所有的图片,如果图片的偏移高度小于屏幕高度+滑动高度,说明已经出现在视窗,就替换图片
    • 优点:兼容性好
    • 缺点:单纯使用 scroll 滑动来监听高度,会引发性能问题,所以要搭配节流
  • Element.getBoundingClientRect
    • 技术要点:与监听图片无太大区别,无非视把图片的偏移高度改成 getBoundingClientRect().top,对比每张图片的自身高度是否出现在视窗(视口)中,有就替换图片
    • 优点:兼容性好,代码相对监听图片高度少了一些
    • 缺点:也是使用 scroll 滑动来监听,会引发性能问题
  • 使用 IntersectionObserver Api
    • 技术要点:通过 IntersectionObserver Api 来实现,图片元素一可见就调用回调,在回调中判断元素是否可见
    • 优点:写起来方便,性能好
    • 缺点:兼容性适配iOS12.2以上,安卓5以上

附上线上 demo:

参考资料