图片懒加载

作者: shaokang 时间: July 4, 2022字数:5121

前言

图片懒加载是一种网页性能优化的方式,它能极大的提高用户体验。最近面试也被问到该问题,这里特意整理下整个懒加载的过程。

实现原理

懒加载的原理就是预先将图片真实的 src 放在自定义的属性里面(比如 data-src),当图片出现在视口范围内,将自定义的属性(data-src)赋值给 src 属性,完成图片加载。

// 页面初始化时
<img data-src="https://image.com/image.jpg" />

// 图片加载
<img data-src="https://image.com/image.jpg" src="https://image.com/image.jpg"/>

其实原理很简单,主要是如何判断图片出现在视图范围内。

首先我们需要获取浏览器视口高度,通过 window.innerHeight(document.documentElement.clientHeight)

    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight;

其次获取图距离顶部的距离,这里有两种办法,下面依次来介绍。

① getBoundingClientRect API 根据 top 值得到顶部距离,距离小于窗口高度

function lazyload() {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight
    const distance = viewPortHeight - img.getBoundingClientRect().top;
    if (distance >= 0) {
        imgList[i].src = img.getAttribute('data-src');
    }
}

完整代码

let imgs = document.querySelectorAll('img');
let lazyload = function () {
    // 获取浏览器滚动高度
    let scrollTop =
        document.body.scrollTop || document.documentElement.scrollTop;
    // 获取浏览器可视高度
    let height = window.innerHeight || document.documentElement.clientHeight;
    for (let i = 0; i < imgs.length; i++) {
        // 如果元素距离文档顶部的高度小于浏览器的滚动高度加浏览器的可视高度,则需要加载
        if (imgs[i].offsetTop < scrollTop + height) {
            // 已经加载的不会再进行加载,或者判断 src
            if (imgs[i].alt != 'loaded') {
                imgs[i].src = imgs[i].getAttribute('data-src'); //将data-src属性值赋值给src
                imgs[i].alt = 'loaded';
            }
        }
    }
};
//调用懒加载函数,加一个防抖函数
function throttle(method, delay) {
    let timer = null;
    return function () {
        clearTimeout(timer);
        timer = setTimeout(function () {
            method();
        }, delay);
    };
}
window.onscroll = throttle(lazyload, 200);

② el.offsetTop <= viewPortHeight + document.documentElement.scrollTop


const viewPortHeight = window.innerHeight || document.documentElement.clientHeight
if (el.offsetTop - document.documentElement.scrollTop <= viewPortHeight) {
    ......
}

③ [推荐] IntersectionObserver 获取目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态。

var io = new IntersectionObserver(callback, options);
io.observe(document.querySelector('img'))

const callback = entries.forEach((item) => {
    if (item.isIntersecting){ // 当前元素可见
        item.target.src = item.target.dataset.src  // 替换src
        io.unobserve(item.target)  // 停止观察当前元素 避免不可见时候再次调用callback函数
    }
})

方案对比:方案 1 通过监听存放图片的滚动容器的滚动事件或者整个页面的滚动事件,在滚动事件触发的时候调用图片元素的 getBoundingClientRect() 函数来进行可见性比对。这种方式是在事件触发的时候同步进行,如果运算量过大极有可能会导致主线程阻塞,从而页面卡顿。
而方案 2 就是一个高性能的元素可见性变化解决方案,所以推荐用方案 2。

IntersectionObserver 和 MutationObserver

上文提到 IntersectionObserver 来实现图片懒加载,与之类似的一个能力是 MutationObserver。下面我们详细看下这两个能力。

IntersectionObserver

IntersectionObserver 接口 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗 (viewport) 交叉状态的方法。祖先元素与视窗 (viewport) 被称为根 (root)。 这是一个异步的接口,只有线程空闲下来,才会执行观察器。

构造函数

const io = new IntersectionObserver(callback, option);

callback

回调函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。如果同时有两个被观察的对象的可见性发生变化,entries 数组就会有两个成员。

entries 对象中每个条目 IntersectionObserverEntry 对象提供目标元素的信息,一共有六个属性。

{
    boundingClientRect:  {x: 580.5, y: -190, width: 202, height: 202, top: -190, …}
    intersectionRatio: 0.05970003083348274
    intersectionRect:  {x: 580, y: 0, width: 203, height: 12, top: 0, …}
    isIntersecting: true
    isVisible: false
    rootBounds: null
    target: div.monitor
    time: 10365.040000062436
    __proto__: IntersectionObserverEntry
}
属性名 含义
time: number 可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
target: Element 被观察的目标元素,是一个 DOM 节点对象
rootBounds: Object 根元素的矩形区域的信息,没有则为 null
boundingClientRect: Object 目标元素的矩形区域的信息,同 getBoundingClientRect() 方法的返回值
intersectionRect: Object 目标元素与视口(或根元素)的交叉区域的信息
intersectionRatio: number 目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0
isIntersecting:boolean 表示是否是进入视图

options

配置的参数有 threshold, root, rootMargin

  • threshold 定义的是触发的回调的阈值,可以定义为 0-1 之间,即 intersectionRatio 交叉比例的值。例 [0, 0.25, 0.5, 0.75, 1] 就表示当目标元素 0%、25%、50%、75%、100% 可见时,分别触发回调函数。
  • root 定义根元素,监测根元素里面的滚动信息。
  • rootMargin 定义根元素的 margin, 扩展或缩小 rootBounds 这个矩形的大小。例:rootMargin: ‘500px 0px’,

MutationObserver

MutationObserver 可以用来监听 DOM 的任何变化,比如子元素、属性和文本内容的变化。 Mutation Observer 是异步触发,DOM 发生变化并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。

构造函数

const mo = new MutationObserver(cb);

示例方法

  1. observe
mo.observe(element, config); // 开始监听

第一个参数是监听的节点, 第二个是监听的参数, 可以选择监听属性, 节点增删, 后代节点的增删, 文本节点的增删及移动.

{
    attributes: true,       // 属性节点的变动
    characterData: true,    // 文本节点的变动
    childList: true,        // 子节点变动
    subtree: true,          // 后代节点变动
    attributeOldValue: true,      // 是否记录原来的属性信息
    characterDataOldValue: true,  // 是否记录原来的文本数据
    attributeFilter:[]      // 指定监听特定的属性
}
  1. disconnect 停止监听,调用该方法后,DOM 再发生变动,也不会触发观察器。

  2. takeRecords 用来在下一次事件触发前立即清除所有变动记录,即不再处理未处理的变动。该方法返回当下积累的变动记录的数组。
    MutationRecord 对象包含了 DOM 的相关信息,有如下属性:

  • type: 表示 DOM 变动的数据类型
  • target: 表示变动的目标节点
  • attributeName, oldValue, attributeNamespace: 记录目标节点的变动数据, 原有数据, 属性的命名空间变化
  • removedNodes, addedNodes: 记录目标节点变动的数据
  • previousSibling, nextSibling: 目标节点的相邻节点数据.