IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。
好官方的描述!!!
不用管它,简单点说,IntersectionObserver
就是观察一个元素是否在当前窗口中。
使用方法
/** * @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
元素上具有真实地址属性,如src
3、我们刚刚看到的
options.thresholds
,即交叉区域与边界区域的比例
class InterObser { constructor (attr = '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('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('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('src')); // 移除多余的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().top
、getBoundingClientRect().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('src'); } } }
遗留问题
这里还遗留几个问题
当用户传入了
options.interOptions
时没有做兼容处理除了判断
el.dataset.src
进行优化之外还可配合节流函数默认图片和
v-lazy
中的图片只能是网络图片
这几个问题后期慢慢补充,个人觉得可以完善这个插件,并且不止限于img
标签,还可以对video
等进行懒加载。
(长按上图,弹出“识别二维码”后可快速关注)