图片懒加载竟如此简单

3,057 阅读4分钟

本文档已更新于 【前端橘子君】 【Github】

这两天在看JavaScript的API时发现了这样一个东西IntersectionObserver(准确说它是浏览器原生提供的一个异步API),它能够对一个元素进行监听,判断该元素是否在当前窗口中。

IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。

好官方的描述!!!

不用管它,简单点说,IntersectionObserver就是观察一个元素是否在当前窗口中。

注意事项:该API是异步的

使用方法

/**
 * @param {Function} callback 必填项,监听元素进入窗口时的回调函数
 * @param {Object} options 非必填项,配置参数
 * @param {Dom Element} options.root 监听元素的根元素,默认为全文档
 * @param {String} options.rootMargin 元素到根元素边缘时的矩形偏移量,支持 px 和 %
 * @param {Array} options.thresholds 交叉区域与边界区域的比例
*/

var io = new IntersectionObserver(callback, options);

io.observe(document.querySelector('img')); // 开始观察,接受一个DOM节点对象
io.unobserve(element); // 停止观察 接受一个element元素
io.disconnect(); // 关闭观察器

懵了么?还好,API的方法和属性比较少,先来撸一个案例你就明白怎么用了。

案例

<div id='app'>
  <img src='./images/01.png'>
  <img src='./images/01.png' >
  <img src='./images/01.png' >
  <img src='./images/01.png' >
  <img src='./images/01.png' >
  <img src='./images/01.png' >
</div>

<script>
  // 创建IntersectionObserver实例
  const io = new IntersectionObserver(function (entrys) {
    console.log(entrys);
  }, {threshold: [1]});

  function update () {
    const els = document.querySelectorAll('img');
    els.forEach(ele => {
      io.observe(ele); // 对元素进行监听
    });
  }

  update();
</script>

此时,控制台会打印出监听的列表,注意列表中的每个元素都是一个监听器,其中target属性就是监听的元素,isIntersecting就是该元素是否在窗口内的标识。

图片懒加载

首先要明确我们的懒加载要有什么内容?

  • 1、默认占位图
  • 2、img元素上具有真实地址属性,如data-src
  • 3、我们刚刚看到的options.thresholds,即交叉区域与边界区域的比例
class InterObser {
  constructor (attr = 'data-src', defaultImg, options = {}) {
    this.attr = attr;
    this.defaultImg = defaultImg;
    this.options = options;
    
    this.install();
    this.update();
  }
  install () {
    const that = this;
    // 初始化 IntersectionObserver 实例
    that.io = new IntersectionObserver(function (entrys) {
      // 当监听到元素进入窗口时,循环每个监听器
      entrys.forEach(ele => {
        // 如果该监听器进入窗口,则将真实地址替换占位图
        if (ele.isIntersecting) {
          that.imgLoad(ele);
          // 停止对该监听器的监听,防止多次操作
          that.io.unobserve(ele.target);
        }
      });
    }, that.options);
  }
  // 图片加载
  imgLoad (ele) {
    const url = ele.target.dataset.src;
    ele.target.src = url;
  }
  update () {
    const els = document.querySelectorAll(`[${this.attr}]`);
    // 为每个监听元素设置默认值
    els.forEach(ele => {
      ele.src = this.defaultImg;
      // 开始监听
      this.io.observe(ele);
    });
  }
}

// 调用
new InterObser('data-src', './images/01.png', {threshold: [1]});

自建vue懒加载插件

既然我们知道了用IntersectionObserver做图片懒加载,那么我们是否可以将该功能做成插件呢?我目前的项目主要以vue为主,那么我就将其做成vue插件。

src目录下创建lib/index.js文件。

class InterObser {
  install (Vue, options) {
    const that = this;
    // 创建指令,使用方法是v-lazy='http://xxx'
    Vue.directive('lazy', {
      bind(el, binding) {
        // 为元素添加默认占位图
        that.init(el, binding.value, options.defaultSrc);
        // 创建IntersectionObserver实例
        that.initObserve(options);
      },
      inserted (el, binding) {
        that.observe(el, binding); // 对该元素开启监听
      }
    })
  }
  init (el, val, def) {
    // 将v-lazy的值挂载到该元素上,因为编译后元素上并没有v-lazy属性
    el.setAttribute('data-src', val)
    // 设置默认占位符
    el.src = def
  }
  initObserve (options) {
    // 该只需创建一次
    var io = new IntersectionObserver(function (entrys) {
      // 循环每个监听的元素,当该元素进入窗口,则替换真实地址,并取消监听
      entrys.forEach(entry => {
        if (entry.isIntersecting) { // 当isIntersecting为true,则代表该元素进入了窗口
          const el = entry.target;
          el.src = el.dataset.src; // 用真实地址替换占位符
          el.removeAttribute('data-src')); // 移除多余的data-src属性
          setTimeout(function () {
            io.unobserve(el); // 取消监听
          }, 0);
        }
      })
    }, options.interOptions || {});

    this.io = io;
  }
  observe (el) {
    this.io.observe(el);
  }
}

export default new InterObser();

使用

main.js中进行配置

import Vue from 'vue';
import LazyLoad from './lib/index.js';
// 全局配置
Vue.use(LazyLoad, {
  defaultSrc: 'http://localhost:3000/01.jpg',
  // IntersectionObserver原生配置项
  interOptions: {
    threshold: [1]
  }
});

在组件中使用

<template>
  <div>
    <img v-lazy='lazySrc'>
    <img v-lazy='lazySrc'>
    <img v-lazy='lazySrc'>
    <img v-lazy='lazySrc'>
    <img v-lazy='lazySrc'>
  </div>
</template>

<script>
export default {
  data () {
    return {
      lazySrc: 'http://localhost:3000/02.jpg'
    };
  }
};
</script>

是不是非常简单,我们来看看传统的图片懒加载是怎么实现的。

传统的图片懒加载

传统图片懒加载主要是依赖监听scroll和获取getBoundingClientRect().topgetBoundingClientRect().bottom来做的,但是这里涉及到性能问题,比如getBoundingClientRect会引起回流等,我们能够做的就是减少这些性能上的操作。

我们可以将上述插件做一下优化,当浏览器不支持IntersectionObserver时,我们就采用传统懒加载的方式。

class InterObser {
  install () {
    .....
  }
  initObserve (options) {
    // 多了一个判断条件而已
    // 仅当浏览器支持该API时才初始化
    if (IntersectionObserver) {
      var io = new IntersectionObserver(function () {
        .......
      }, options.interOptions || {});
      this.io = io;
    }
  }
  observe (el) {
    // 如果浏览器支持IntersectionObserver,则使用,否则使用传统懒加载方式
    if (IntersectionObserver) {
      this.io.observe(el);
    } else {
      this.listenerScroll(el);
    }
  }
  listenerScroll (el) {
    const that = this;
    that.load(el); // 为了使当前窗口的图片能够正常加载
    window.addEventListener('scroll', function (e) {
      // 防止多次触发
      // 减少getBoundingClientRect操作从而达到优化的目的
      if (el.dataset.src) {
        that.load(el);
      }
    });
  }
  load (el) {
    // 获取窗口高度
    var docHeight = document.documentElement.clientHeight;
    var boundingClientRect = el.getBoundingClientRect();
    var bottom = boundingClientRect.bottom;
    var top = boundingClientRect.top;
    // 当元素进入窗口时,才加载真实图片
    if (top < docHeight && bottom > 0) {
      el.src = el.dataset.src;
      el.removeAttribute('data-src');
    }
  }
}

遗留问题

这里还遗留几个问题

  • 当用户传入了options.interOptions时没有做兼容处理
  • 除了判断el.dataset.src进行优化之外还可配合节流函数
  • 默认图片和v-lazy中的图片只能是网络图片

这几个问题后期慢慢补充,个人觉得可以完善这个插件,并且不止限于img标签,还可以对video等进行懒加载。


更多相关文档,请见:

线上地址 【前端橘子君】

GitHub仓库【前端橘子君】