概述
网页中涉及到大量图片的时候,往往可以考虑图片的懒加载,所谓的图片懒加载就是初始的时候,不渲染图片,而是默认给一张本地静态图片,当用户滚动条滚动到可视区范围位置的时候,再加载图片。这也是页面性能优化的一种方案,网站中使用到这种方式的网站有京东,淘宝等多数的电商网站,如果我们自己项目中也涉及到大量图片加载,其实可以考虑使用图片懒加载,这里借助vue中的自定义指令实现这个插件。
最终效果(效果图较大,等会就出来了)
window滚动
带滚动父元素的
图片懒加载原理
分两种情况
- 全局来说,我们只考虑在window上面绑定滚动事件,当图片进入到浏览器窗口可视区范围,我们就加载图片,默认没进入可视区的图片,我们默认使用一张尺寸较小本地静态图(可以自己定义)。
- 局部来说,我们给某个大盒子设置overflow属性,让其超出隐藏,如果内部又很多图片,那么我们可以通过滚动父级元素,当图片进入到父级元素的可视区范围,则加载图片,反之和1相同。
元素进入可视区的计算方法
- 全局
对于全局的这种情况比较简单,通过getBoundingClientRect这个方法可以获取元素到可视区顶部的距离,只要顶部的距离小于html文档的可视区高度,则进入可视区
export const checkEnterView = (imgInstance) => {
const { top, left } = el.getBoundingClientRect();
const htmlClientHeight = document.documentElement.clientHeight;
const htmlClientWidth = document.documentElement.clientWidth;
if (top < htmlClientHeight && left < htmlClientWidth) {
return true;
}
return false;
};
- 局部
对于第二种情况,我们要判断元素是否进入到父元素(带overflow),还是有点不好想,思路和全局的也一样
export const checkEnterView = (imgInstance, scrollParent) => {
let parentHeight, parentWidth, x, y;
if (imgInstance.scroll) {
// 存在滚动父级,需要元素到滚动父级可视区顶部的估计:计算公式为offsetTop-scrollParent.scrollTop
parentHeight = scrollParent.clientHeight;
parentWidth = scrollParent.clientWidth;
y = imgInstance.el.offsetTop - scrollParent.scrollTop;
x = imgInstance.el.offsetLeft - scrollParent.scrollLeft;
}
if (y < parentHeight && x < parentWidth) {
return true;
}
return false;
};
可以根据下面这张图脑海里想一想
实现
知道了上面图片懒加载的原理,实现起来就比较简单了,无非是图片元素进入到可视区范围,将其正确的路径设置上,能加载出来就不变,加载不出来就设置出错的默认图(避免页面出现碎掉的标签),还可以进行过渡效果设置。
结构
utils.js
用到的几个工具函数,判断可视区,防抖,节流
import loadingSrc from "../../assets/img/loading.jpg";
/**
* @Description 判断dom元素是否到达可视区
* @Author Galaxy
* @Date 2022/10/18 18:47:46
* @param { Boolean }
* @return { Boolean }
**/
export const checkEnterView = (imgInstance, scrollParent) => {
let parentHeight, parentWidth, x, y;
if (imgInstance.scroll) {
// 存在滚动父级,需要元素到滚动父级可视区顶部的估计:计算公式为offsetTop-scrollParent.scrollTop
parentHeight = scrollParent.clientHeight;
parentWidth = scrollParent.clientWidth;
y = imgInstance.el.offsetTop - scrollParent.scrollTop;
x = imgInstance.el.offsetLeft - scrollParent.scrollLeft;
} else {
// 不存在滚动父级的情况
const { top, left } = imgInstance.el.getBoundingClientRect();
y = top;
x = left;
parentHeight = document.documentElement.clientHeight;
parentWidth = document.documentElement.clientWidth;
}
if (y < parentHeight && x < parentWidth) {
return true;
}
return false;
};
// 初始状态将图片的路径设置成加载中的图片
export const initLoadImg = (el) => {
el.src = loadingSrc;
};
/**
* @Description 防抖函数
* @Author Galaxy
* @param { Function } fn 需要防抖的函数
* @param { Number } time 时间间隔
* @return { Fuction } 返回防抖后的新函数
**/
export const debounce = (fn, time) => {
let timer = null;
return function (...arg) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, ...arg);
}, time);
};
};
/**
* @Description 节流函数
* @Author Galaxy
* @param { Function } fn 需要节流的函数
* @param { Number } time 时间间隔
* @param { Number } immediate 是否初始的时候立即执行一次
* @return { Fuction } 返回节流后的新函数
**/
export const throttle = (fn, time, immediate = true) => {
let oldTime = new Date();
return function (...arg) {
if (immediate) {
fn.apply(this, ...arg);
immediate = false;
return;
}
let currentTime = new Date();
if (currentTime - oldTime >= time) {
fn.apply(this, ...arg);
oldTime = currentTime;
}
};
};
ImageItemClass.js
指令绑定的img元素的抽象类
// v-lazy绑定的元素加载图片项构造函数
export default class ImageItemClass {
constructor({ errorImg, src, el, transitionTime }) {
// img标签
this.el = el;
// 加载出错显示的默认图
this.errorImg = errorImg;
//绑定的图片路径
this.src = src;
// 是否加载过了当前图片
this.loaded = false;
// 动画过渡时长
this.transitionTime = transitionTime;
// 是否存在加载出错
this.loadingError = false;
}
// 每个图片项的加载函数
loadImg() {
// 返回promise,外部调用可以做其他扩展处理
return new Promise((resolve, reject) => {
this.el.src = this.src;
//src为有效图片路径能够加载出来
this.el.onload = () => {
resolve();
// 为了更好的用户体验,这里将图片的透明度进行过渡
this.el.style.opacity = '0';
// this.el.style.transition = `opacity ${this.transitionTime}s`;
this.addTransition();
};
//src为无效图片路径不能够加载出来
this.el.onerror = () => {
// 设置成默认加载错误时候的图片,避免破碎图片的显示
this.el.src = this.errorImg;
this.loadingError = true;
reject();
this.addTransition();
};
// 标识当前图片已经被加载了,避免滚动重复处理造成的卡顿
this.loaded = true;
});
}
// 添加过渡
addTransition() {
requestAnimationFrame(() => {
!this.loadingError ? (this.el.style.transition = `opacity ${1.2}s ease-in-out`) : null;
this.el.style.opacity = '1';
});
}
}
Lazy.js
v-lazy指令的构造函数,便于扩展
import { checkEnterView, initLoadImg, debounce, throttle } from "./utils";
import ImageItemClass from "./ImageItemClass";
// 懒加载类
export default class Lazy {
constructor(options) {
// 所有v-lazy绑定的图片的集合
this.imgPoolList = new Map();
this.noScrollParentimgPoolList = []; //不带滚动父级
this.scrollParentimgPoolList = []; //带滚动父级
// 图片集合字段名
this.pooListField = "noScrollParentimgPoolList";
// 是否绑定了滚动处理函数到window
this.isBindScrollWindow = false;
// 是否绑定了滚动处理函数到带overflow属性的父级元素
this.isBindScrollScrollParent = false;
// 图片加载失败的默认图
this.errorImg = options.errorImg || require("./img/error.jpg");
// 图片加载中显示的默认图
this.loadingImg = options.loadingImg || require("./img/loading.jpg");
// 是否指定特定的带over-flow的父级作为滚动处理函数绑定的对象,默认绑定滚动处理函数在window上面
this.scrollParent = null;
// 根据用户指令配置项,确定是否根据父级来绑定滚动函数
this.isScrollParent = false;
// 动画过渡时长
this.transitionTime = options.transitionTime || 0.9;
// 防抖的阈值
this.debounceTime = options.debounceTime || 200;
// 绑定防抖函数,避免滚动过程频繁触发,提高流畅度
this.debounceHandleScroll = debounce(
this.handleScroll,
this.debounceTime
).bind(this);
// 绑定节流函数,避免滚动过程频繁触发,提高流畅度
this.throttleHandleScroll = throttle(
this.handleScroll,
this.debounceTime,
true
).bind(this);
}
// 指令绑定的dom元素插入到页面中触发(类似mounted)
inserted(el, binding, vnode) {
// 是否需要滚动父级
this.isScrollParent = binding.modifiers.scroll;
this.pooListField = this.isScrollParent
? "scrollParentimgPoolList"
: "noScrollParentimgPoolList";
// 将当前dom元素加入到set集合中,便于统一处理
this.imgPoolList.set(
el,
new ImageItemClass({
src: binding.value, //图片路径
errorImg: this.errorImg, //加载出错默认图
el, //当前dom节点
transitionTime: this.transitionTime, //动画过渡时长
scroll: this.isScrollParent, //是否标记滚动父级
})
);
// 初始化渲染图片状态
initLoadImg(el);
// 初始的时候,执行一次函数
this.handleScroll();
// 记住绑定过了就不需要绑定了,不然会出现给dom元素重复多次绑定滚动处理函数
// 如果存在滚动滚动父级,那么给滚动父级也绑定滚动处理函数
if (this.findScrollParent(el)) {
!this.isBindScrollScrollParent &&
this.scrollParent.addEventListener("scroll", this.debounceHandleScroll);
this.isBindScrollScrollParent = true;
} else {
!this.isBindScrollWindow &&
window.addEventListener("scroll", this.debounceHandleScroll);
this.isBindScrollWindow = true;
}
}
// 元素卸载,初始化
unbind() {
this.scrollParentimgPoolList = [];
this.noScrollParentimgPoolList = [];
}
// 指令绑定的值变化
update(el, binding) {
this.updateImageInstance(el, binding);
}
// 更新保存的图片实例对象
updateImageInstance(el, binding) {
// 将更新的图片的路径进行更换,重新加载
for (let [elment, imgInstance] of this.imgPoolList) {
if (el == elment && !imgInstance.loaded) {
imgInstance.src = binding.value;
}
}
this.handleScroll();
}
// 滚动的时候,根据dom元素是否进入到可视区动态,再决定是否加载图片
handleScroll() {
for (let [el, imgInstance] of this.imgPoolList) {
if (this.scrollParent) {
// 当img出现在可视区并且没有被加载的时候,进行加载处理
if (
checkEnterView(imgInstance, this.scrollParent) &&
!imgInstance.loaded
) {
this.resolveImgInstance(imgInstance);
}
} else {
// 当img出现在可视区并且没有被加载的时候,进行加载处理
if (checkEnterView(imgInstance) && !imgInstance.loaded) {
this.resolveImgInstance(imgInstance);
}
}
}
}
// 加载图片
resolveImgInstance(imgInstance) {
imgInstance
.loadImg()
.then(() => {
// 图片加载成功的回调
})
.catch(() => {
// 图片加载失败的回调,将loaded变为false,便于图片更新后重新加载
imgInstance.loaded = false;
});
}
// 寻找滚动父级元素,带overflow的
findScrollParent(el) {
if (!this.isScrollParent) return false;
let parent = el.parentNode;
while (parent) {
if (
getComputedStyle(parent).getPropertyValue("overflow") == "scroll" ||
getComputedStyle(parent).getPropertyValue("overflow") == "auto" ||
getComputedStyle(parent).getPropertyValue("overflow-x") == "scroll" ||
getComputedStyle(parent).getPropertyValue("overflow-x") == "auto" ||
getComputedStyle(parent).getPropertyValue("overflow-y") == "scroll" ||
getComputedStyle(parent).getPropertyValue("overflow-y") == "auto"
) {
// 找到了带overflow样式的父级元素
this.scrollParent = parent;
return true;
}
parent = parent.parentNode;
}
return false;
}
}
index.js
注册指令集合
import Lazy from "./Lazy";
export default {
// 插件都要具有install方法,这样外部就可以通过vue.use注册插件了
install(Vue, options = {}) {
// 注册v-lazy指令
const lazyInstance = new Lazy(options);
Vue.directive("lazy", {
inserted: lazyInstance.inserted.bind(lazyInstance),
});
},
};
使用
main.js
import Vue from "vue";
import App from "./App.vue";
// 导入插件
import vLazy from "./components/v-lazy";
Vue.use(vLazy);
new Vue({
render: (h) => h(App),
router,
store,
}).$mount("#app");
App.vue
注意,当设置了父级元素overflow的时候,一定要设置position定位,不然通过offsetTop获取距离定位元素就会出现误差
<template>
<div class="app">
<div class="aline">
<!-- <div class="item">
<div class="img-wrap">
<h2>全局window滚动</h2>
<ul>
<li v-for="(item, index) in list" :key="index">
<img v-lazy="item.src" />
</li>
</ul>
</div>
</div> -->
<div class="item">
<h2>带overflow父级内部滚动</h2>
<div class="img-wrap scroll-wrap">
<ul>
<li v-for="(item, index) in list" :key="index">
<img v-lazy.scroll="item.src" />
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {
current: -1,
list: [],
img: "",
};
},
mounted() {
this.getList();
this.init();
},
methods: {
handleCLick() {
console.log(this.current++);
},
getList() {
let res = [
{
id: Math.random(),
src: require("./assets/img/01.webp"),
},
{
id: Math.random(),
src: require("./assets/img/02.webp"),
},
{
id: Math.random(),
src: require("./assets/img/03.webp"),
},
{
id: Math.random(),
src: 11,
},
{
id: Math.random(),
src: require("./assets/img/04.webp"),
},
];
this.list = [...res, ...res, ...res, ...res];
},
init() {},
},
};
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
}
.app {
padding: 20px;
button {
padding: 10px;
background-color: #008c8c;
color: #fff;
margin: 20px 0;
}
.container {
.operate {
text-align: center;
}
.aline {
width: 50%;
}
h2 {
font-weight: bold;
font-size: 20px;
}
.aline {
&:nth-child(1) {
margin-right: 20px;
}
}
display: flex;
justify-content: space-between;
}
}
.aline {
display: flex;
justify-content: center;
}
.demo-split {
height: 200px;
border: 1px solid #dcdee2;
margin: 20px;
}
.item {
margin: 40px;
img {
width: 250px;
height: 200px;
}
ul {
margin: 0 auto;
li {
border: 1px solid red;
height: 200px;
width: 250px;
}
}
.scroll-wrap {
height: 500px;
overflow: auto;
position: relative;
}
}
</style>
总结
图片懒加载在项目中很实用,也算是性能优化的一种方案,这里自己写一个,加深对其原理的理解,插件已上传到npm
npm 插件下载地址
npm i gnip-vue2-lazy-load