手把手教你实现图片懒加载指令

什么是懒加载?

懒加载又称延迟加载,是一种在页面加载时推迟加载一些非关键资源的技术。并且会在需要时加载这些非关键资源。就图像而言,“非关键”就是指用户在屏幕内看不到的内容。

你可能已经看到过懒加载的实现,它是这样的:

  • 你抵达一个页面,并在阅读内容时开始滚动
  • 在某个时候,你将占位符图像滚动到视区中
  • 占位符图像突然被最终的图像所替换

为什么要懒加载图像?

网站里的图像可能占据了相当大的比重。如果直接加载网站所有图像的话:

  • 它会浪费数据。如果用户的连接使用的是计费流量,那么加载用户从未见过的内容就是在浪费他们的钱。此处还未见过的内容指得即是屏幕外的内容
  • 它会浪费资源处理时间和其他系统资源。下载媒体资源后,浏览器必须对其进行解码并在视区中呈现其内容

而延迟加载图像会减少了初始页面的加载时间、初始页面的大小和系统资源使用,所有这些都可以提高网站的性能。

怎么实现懒加载?

最常见的懒加载图像是在img元素中使用的图像。对于此类图像,我们有三个延迟加载选项,可以组合使用以实现跨浏览器的最佳兼容性:

1. 使用浏览器级别懒加载

Chrome 和 Firefox 都支持通过 loading 属性实现延迟加载。此属性可以添加到img元素中。 lazy 值会告诉浏览器,如果图像位于可视区,则立即加载图像,并在用户滚动到它们附近时获取其图像。如果浏览器不支持该属性,则该属性将被忽略,图像将像往常一样立即加载。

<img loading="lazy">
复制代码

兼容 Chrome77+

image.png

2. 使用 Intersection Observer

IntersectionObserver 接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。意思就是浏览器可以观察元素是否在可视区内。

相较于最后一种依赖各种事件处理程序的代码,Intersection Observer 更易于使用和阅读,因为你只需注册一个观察器即可观察元素,无需编写繁琐的元素可见性检测代码。

/*
  初始化 Intersectionobserver 类
  `callback`是当元素的可见性变化时候的回调函数,`options`是一些配置项
*/
const observer = new IntersectionObserver(callback, options);
/* 监听对应的 DOM 元素 */
observer.observe(DOM);
/* 取消对 DOM 元素的监听 */
observer.unobserve(DOM);
/*
  root:用于观察的根元素,默认是浏览器的视口
  rootMargin:用来缩放视窗的的大小,用法和 CSS 类似,
              `10px 10px 30px 20px`表示top、right、bottom 和 left 扩大的对应值,
              `10px 20px 5px`表示top,left&right, bottom 扩大的对应值
  thresholds: 当被监听的元素与根元素的交叉比例达到该值时触发回调函数,
              默认为`[0]`, `0.1`表示当交叉比例达到`0.1`时触发回调,
              `[0,0.5]`表示当交叉比例达到`0`和`0.5`时都会触发回调
*/
const options = {
  root: null,
  rootMargin: 0,
  thresholds: 1,
};
/*
  回调函数在监听的 DOM 元素可见性变化时触发
  entries 中有多个参数,我们使用到以下两个参数
  isIntersecting 表示观察的元素是否出现在 root 元素可视区,
  intersectionRatio 表示观察的元素出现在 root 元素的比例
*/
function callback(entries) {
  entries.forEach(entry => {
    if (entry?.isIntersecting || entry?.intersectionRatio > 0) {
      // 在此处进行图片地址替换
    }
  });
}
复制代码

兼容 Chrome51+

image.png

3. 使用 getBoundingClientRect

当你的网站非常注重浏览器的兼容性时,以上的方法就无法满足你的需求,我们需要使用scroll及其他事件处理程序配合getBoundingClientRect来实现代码降级,从而确定确定元素是否位于视区中。此方法几乎可以兼容所以浏览器,但是性能不如上面二者。

// 用于存当前页面已有的图片
observerMap = new Map();
// 通过滚动事件触发 scrollHandle 回调
window.addEventListener('scroll', scrollHandle, true);
// 每次回调里进行遍历已有图片列表
function scrollHandle() {
  for (let dom of observerlist) {
    // 判断元素是否在可视区从而判断是否替换真实地址
  }
}
// 判断元素是否在可视区
function checkDomInView(element) {
  const viewwidth = window.innerWidth || document.documentElement.clientWidth;
  const viewHeight = window.innerHeight || document.documentElement.clientHeight;
  const { top, right, bottom, left } = element.getBoundingClientRect();
  return left < viewwidth && right > 0 && top < viewHeight && bottom > 0;
}
复制代码

兼容 Chrome4+

image.png

具体实践

前置依赖

核心思路

  1. 注册全局指令 lazy,在指令恰当的生命周期内进行懒加载处理逻辑

  2. 通过上面三种方式实现代码的优雅降级:

    a. 优先使用浏览器支持的loading属性

    b. 不支持loading属性采用IntersectionObserver作为备选方案

    c. 为了更好的兼容,使用getBoundingClientRect及浏览器事件实现降级

  3. 实现一个lazyload类,内部存在两个观察器,一个是依赖原生IntersectionObserver实现的Observer观察器,一个是依赖getBoundingClientRect及浏览器事件实现的ObserverBackup观察器。存在以下几个核心问题:

    a. 真实图片地址存储在哪个位置

    b. 如何判断元素是否在可视区范围内

    c. 什么时机进行判断元素是否在可视区范围内

    d. 什么时机进行真实地址的替换

question.png

代码逻辑

携带上述几个核心的问题进入真实的开发环节

  1. 创建插件入口文件 src/lazyload-plugin/index.js

vue 插件要求导出一个包含intall函数的对象,全局只会install一次;

首先判断了是否支持loading属性,如果支持将属性即真实地址赋值到el绑定的元素上,不支持即会创建一个lazyload实例,同时将真实地址存储到el元素上的data-src(解决问题a),并将图片地址设置为一个默认图像,最后利用lazyload实例观察el元素;

lazyload实例有三个方法,initObserver用于初始化观察器;onceObserve用于观察绑定元素;loadRealImg用于替换真实图像地址。

import Observer from './observer';
import ObserverBackup from './observer-backup';

const defalutImage = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';

class LazyLoad {
  constructor() {
    this._observer = null;
    this._defaultImage = defalutImage;
    this.initObserver();
  }

  initObserver() {
    if (window.IntersectionObserver) {
      this._observer = new Observer();
    } else {
      this._observer = new ObserverBackup();
    }
  }

  onceObserve(dom) {
    this._observer?.onceWatch(dom, this.loadRealImg.bind(null, dom));
  }

  loadRealImg(dom) {
    const realUrl = dom.getAttribute('data-src');
    dom.setAttribute('src', realUrl);
    dom.removeAttribute('data-src');
  }
}

const isBrowserSupportLazy = 'loading' in HTMLImageElement.prototype;

export default {
  install: app => {
    const lazyload = isBrowserSupportLazy ? null : new LazyLoad();

    app.directive('lazy', (el, binding) => {
      if (isBrowserSupportLazy) {
        el.setAttribute('loading', 'lazy');
        el.setAttribute('src', binding.value);
      } else {
        el.setAttribute('data-src', binding.value);
        el.setAttribute('src', lazyload._defaultImage);
        lazyload.onceObserve(el);
      }
    });
  },
};
复制代码
  1. 创建第一个观察器文件src/lazyload-plugin/observer.js

Observer类包含两个属性其中observerMap用于存储真实dom元素及对应的回调函数(即lazyload实例中的loadRealImg函数和停止监听dom的逻辑);observer用于实例化原生的IntersectionObserver观察器。

另外还有三个方法其中最重要的是intersectionHandle方法,它是IntersectionObserver观察器的第一个参数,即观察元素与根元素的交叉比例达到threshold值时就会执行(解决问题c),方法中遍历了全部被观察对象并通过isIntersectingintersectionRatio判断被观察的元素是否可见(解决问题b),如果可见就执行回调函数即此时替换真实图片地址(解决问题d)。

onceWatch方法主要是执行观察元素,同时将元素与回调函数进行绑定;unwatch方法主要是停止对元素的观察并将元素与回调函数解绑。

export default class Observer {
  constructor({ root = null, rootMargin = '0px', threshold = 0 } = {}) {
    this.observerMap = new WeakMap();
    this.observer = new IntersectionObserver(this.intersectionHandle.bind(this), { root, rootMargin, threshold });
  }

  intersectionHandle(entries) {
    entries.forEach(entry => {
      if (entry?.isIntersecting || entry?.intersectionRatio > 0) {
        const onceCallback = this.observerMap.get(entry.target);
        onceCallback();
      }
    });
  }

  onceWatch(dom, callback) {
    const onceCallBack = () => {
      callback();
      this.unwatch(dom);
    };
    if (dom && !this.observerMap.has(dom)) {
      this.observerMap.set(dom, onceCallBack);
      this.observer.observe(dom);
    }
  }

  unwatch(dom) {
    if (typeof dom === 'object') {
      this.observer.unobserve(dom);
      this.observerMap.delete(dom);
    }
  }
}
复制代码
  1. 创建第二个观察器文件src/lazyload-plugin/observer-backup.js

ObserverBackUp类包含两个属性其中observerMap用于存储真实dom元素及对应的回调函数(即lazyload实例中的loadRealImg函数和停止监听dom的逻辑);_throttleFn是节流后的scrollHandle函数作为滚动等原生浏览器事件的回调(解决问题c),遍历所有被观察的元素进行是否在可视区的逻辑判断。

另外还有三个方法其中最重要的是checkDomInView方法其中依赖了isInViewPort函数对元素进行是否在可视范围的检测(解决问题b),如果可见就执行回调函数即此时替换真实图片地址(解决问题d),isInViewPort利用了getBoundingClientRect及window属性进行具体判断。

rect.png

onceWatch方法主要是将元素与回调函数进行绑定,同时对首次绑定的元素执行checkDomInView方法;unwatch方法主要是将元素与回调函数解绑,另外还有一个节流函数throttle,就不细说了不是本文的重点。

const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
function isInViewPort(element) {
  const { top, right, bottom, left } = element.getBoundingClientRect();
  return left < viewWidth && right > 0 && top < viewHeight && bottom > 0;
}

export default class ObserverBackUp {
  constructor() {
    this.observerMap = new Map();
    this._throttleFn = throttle(this.scrollHandle.bind(this), 100, { isLeading: true });
    ['scroll', 'touchmove', 'transitionend', 'animationend'].forEach(event =>
      window.addEventListener(event, this._throttleFn, true)
    );
  }

  scrollHandle() {
    const observerList = this.observerMap.keys();
    for (let dom of observerList) {
      const onceCallBack = this.observerMap.get(dom);
      this.checkDomInView(dom, onceCallBack);
    }
  }

  checkDomInView(dom, onceCallback) {
    if (isInViewPort(dom)) {
      typeof onceCallback === 'function' && onceCallback();
    }
  }

  onceWatch(dom, callback) {
    const onceCallBack = () => {
      callback();
      this.unwatch(dom);
    };
    if (dom && !this.observerMap.has(dom)) {
      this.observerMap.set(dom, onceCallBack);
      this.checkDomInView(dom, onceCallBack);
    }
  }

  unwatch(dom) {
    if (typeof dom === 'object') {
      this.observerMap.delete(dom);
    }
  }
}

function throttle(fn, wait = 300, { isLeading = true } = {}) {
  let timerId;

  return function (...args) {
    if (isLeading) {
      fn.apply(this, args);
      isLeading = false;
      return;
    }

    if (timerId) {
      return;
    }

    timerId = setTimeout(() => {
      clearTimeout(timerId);
      timerId = null;

      fn.apply(this, args);
    }, wait);
  };
}
复制代码
  1. 如何使用

mian.js文件中引入该插件并注册。

import { createApp } from 'vue';
import App from './App.vue';
import lazyloadPlugin from './lazyload-plugin';

const app = createApp(App);

app.use(lazyloadPlugin);

app.mount('#app');
复制代码

vue组件中使用:<img v-lazy="真实图片地址" />,通过v-lazy指令使用简单方便。

文章小结

希望通过这篇文章可以帮助各位了解到图片懒加载的基本原理及做法。

而这只是HTML里展示图像的一种方式,还有一种常见的图片渲染方式是CSS中的图像,浏览器的loading属性不适用于CSS背景图片,因此如果你有要延迟加载的背景图片,那么需要考虑其他方法。提供一种思路,通过使用JavaScript来确定元素何时出现在视区内,然后对那个元素应用一个会调用背景图像的类名,可以使用此推测行为来延迟CSS中的图像加载。相信各位也已经有思路了,如果有其他更优的方案,欢迎在评论区进行探讨~

.lazy-background {
  background-image: url("占位图");
}

.lazy-background.visible {
  background-image: url("占位图");
}
复制代码

日常开发中大多数都不需要我们自己实现这样的功能,因为有很多已有的开源库都帮我们实现了,并且实现的更加完善。下面推荐一些懒加载开源库:

关注公众号《小前日记 》获取源码

IMG_5155.JPG

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享