一:懒加载的目的
有些页面可能展示的是大量的图片,如果我们一次性加载所有图片就会浪费性能,影响用户体验,所以我们就会懒加载这些图片。即可视区域之外的图片不加载,随着页面的滚动,图片进入可视区域,则触发图片的加载显示。
优点:页面加载速度快,用户体验感更好且节省流量
二:懒加载的原理
- 存储图片的真实路径,把图片的真实路径绑定给一个以data开头的自定义属性data-src即可,页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片(没有请求就提高了性能)
typescript
<img data-src="xxx" src="xxx" width="180" height="180"/>
- 需要一个滚动事件,判断元素是否在浏览器窗口,一旦进入视口才进行加载,当滚动加载的时候,就把这张图片替换为真正的url地址(也就是data-src里保存的值)
- 等到图片进入视口后,利用js提取data-src的真实图片地址赋值给src属性,就会去发送请求加载图片,真正实现了按需加载
实现图片懒加载的多种方法的区别就在于判断元素是否进入视口的方式不同,当使用方法3和方法4时,如果发生滚动事件,会产生大量的循环和判断操作判断图片是否可视区里,因此需要用节流进行优化。
三、 实现懒加载
方法1 lazyload插件
vue2
vue-lazyload的核心原理是利用了IntersectionObserver
API,这是一个用于检测元素是否与视口相交的API,它可以高效地监听元素的可见性变化,并触发回调函数。
vue-lazyload在注册插件时,会创建一个全局的IntersectionObserver
实例,并设置一些选项,如阈值(threshold)、根元素(root)等。然后,在绑定v-lazy
指令时,会创建一个监听器(listener)对象,并将其添加到一个监听器队列(listenerQueue)中。每个监听器对象都包含了元素的相关信息,如状态(state)、图片地址(src)等。
接下来,vue-lazyload会遍历监听器队列,并调用IntersectionObserver
实例的observe
方法,将每个元素注册到观察者中。当元素与视口相交时,IntersectionObserver
实例会触发回调函数,并传入一个entries参数,表示所有被观察的元素的状态信息。vue-lazyload会根据entries中的isIntersecting
属性判断元素是否可见,如果是,则调用监听器对象的load方法,将元素的src或者style属性替换为真实的图片地址,并将该监听器对象从队列中移除。
基本用法
- 安装
typescript
npm i vue-lazyload@1.2.3 -S
- 使用
main.ts
typescript
// 1.图片懒加载插件
import VueLazyload from 'vue-lazyload'
// 2.注册插件
Vue.use(VueLazyload, {
//参数配置 可不填
// 懒加载默认加载图片
loading: 'xxx.png',
// 加载失败后加载的图片
error: 'xxx.png',
preLoad: 1.3, // 预加载高度的比例
attempt: 3 // 尝试加载次数
})
可选参数配置项
vue-lazyload提供了一个自定义指令v-lazy,可以在img标签或者任何需要设置背景图片的标签上使用它。例如:
typescript
<!-- 懒加载img标签 -->
<img v-lazy="imgUrl" />
<!-- 懒加载背景图片 -->
<div v-lazy:background-image="bgUrl"></div>
v-lazy指令接收一个字符串类型的值,表示图片的地址。如果是背景图片,需要在指令后加上:background-image修饰符。
当页面滚动时,vue-lazyload会检测元素是否进入可视区域,如果是,则替换元素的src或者style属性,从而实现懒加载。
⚠️ 若图片为循环渲染、分页显示,则必须写key值,不然切换页面后页面视图不刷新
vue3
原理与使用方法与vue2版本插件相同,仅安装不同
- 安装
typescript
npm i vue-lazyload-next -S
- 使用
main.ts
typescript
import VueLazyloadNext from 'vue-lazyload-next';
app.use(VueLazyloadNext, {
// 添加一些配置参数 可不填
// 懒加载默认加载图片
loading: 'xxx.png',
// 加载失败后加载的图片
error: 'xxx.png',
preLoad: 1.3, // 预加载高度的比例
attempt: 3 // 尝试加载次数
});
这个插件支持背景图;且图片不会反复加载【加载过的不会再加载】;
方法2 IntersectionObserve()
API
原理
Intersection Observer是是浏览器原生提供的构造函数,使用它能省到大量的循环和判断。这个构造函数的作用是它能够观察可视窗口与目标元素产生的交叉区域。简单来说就是当用它观察我们的图片时,当图片出现或者消失在可视窗口,它都能知道并且会执行一个特殊的回调函数,就可以利用这个回调函数实现需要的操作。
hook
vue3中选择用hook进行集成
useLazyload.ts文件
typescript
// 定义自定义指令
const defineDirective = (app: any) => {
app.directive('lazy', {
mounted(el: HTMLImageElement, bindings: any) {
// el表示使用指令的DOM元素
// 指令的功能:实现图片的懒加载
// 1、监听图片是否进入可视区
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
// true;进入可视区域,false:未进入可视区域
if (isIntersecting) {
// 1、给图片的src属性赋值图片的地址
el.src = bindings.value;
// 2、取消图片的监听,默认是会一直监听的,如果不取消。就会一直执行
// eslint-disable-next-line spellcheck/spell-checker
observer.unobserve(el);
}
});
// 监听dom元素
observer.observe(el);
}
});
};
export default {
install(app: any) {
// 自定义指令
defineDirective(app);
}
};
基本用法
main.ts
typescript
import lazy from './hooks/useLazy';
app.use(lazy);
对需要进行懒加载的img标签,直接将:src替换为v-lazy
typescript
<img
class="image"
:key="data.rendered_result_image_url"
v-lazy="data.rendered_result_image_url"
/>
⚠️若图片为循环渲染、分页显示,则必须写key值,不然切换页面后页面视图不刷新
方法3 loading="lazy
HTML新增 loading属性
基本用法
typescript
<img src="xxx.png" loading="lazy">
loading 属性支持 3 种属性值:
- auto 浏览器默认的懒加载策略,和不增加这个属性的表现一样
- lazy 在资源距当前视窗到了特定距离内后再开始加载
- eager 立即加载,无论资源在页面中什么位置
图片必须声明width和height,不然会看到布局发生移动
兼容性
不适用于背景图情况;
方法4 滚动监听+scrollTop+offsetTop+innerHeight
如何判断元素是否到达可视区域
window.innerHeight
是浏览器可视区的高度;document.body.scrollTop || document.documentElement.scrollTop
是浏览器滚动的过的距离;imgs.offsetTop
是元素顶部距离文档顶部的高度(包括滚动条的距离);- 内容达到显示区域:
img.offsetTop < window.innerHeight + document.body.scrollTop
; - 到达可视区域后,
imgs[i].src = imgs[i].getAttribute('data-src') as string; 将data-src属性值赋值给src
,实现懒加载
(在我项目中:)
!!!!但在集成过程中发现,由于img父元素标签没有设置高度,scrollTop始终为0,无法实现动态计算window.innerHeight + document.body.scrollTop的值【一体化-算法版本-标准图】;不适用于没有设置父级高度的情况
方法5 滚动监听+getBoundingClientRect()
typescript
//获取所有img标签
const imgs = document.getElementsByTagName('img');
onMounted(() => {
//用于首屏加载
lazyLoad();
//添加滚动事件监听
document.addEventListener('scroll', throttle(lazyLoad, 500), true);
});
onUnMounted(() => {
document.removeEventListener('scroll', throttle(lazyLoad, 500), true);
});
// 节流
const throttle = (fn: { apply: (arg0: any, arg1: any[]) => void }, t: number) => {
let flag = true;
const interval = t || 500;
return function (this: any, ...args: any) {
if (flag) {
fn.apply(this, args);
flag = false;
setTimeout(() => {
flag = true;
}, interval);
}
};
};
const lazyLoad = () => {
const offsetHeight = window.innerHeight || document.documentElement.clientHeight;
Array.from(imgs).forEach(item => {
const oBounding = item.getBoundingClientRect(); //返回一个矩形对象,包含上下左右的偏移值
if (0 <= oBounding.top && oBounding.top <= offsetHeight) {
// //性能优化 进行判断 已经加载的不会再进行加载
if (item.getAttribute('alt') !== 'loaded') {
item.setAttribute('src', item.getAttribute('data-src') as string);
item.setAttribute('alt', 'loaded');
}
}
});
};
缺点
- 挂载时需要立刻调用lazyLoad函数,不然首屏不会加载;若需要图片分页,切换分页后不会也主动加载,需要调用lazyLoad方法
- 同样需要给图片设置key值,不然分页图片不刷新
- 写在image组件内部的话会反复调用lazyload方法,需要写在外层组件