一、懒加载
1、什么是懒加载?
懒加载也叫做"延迟加载"或者"按需加载",是一种常用的网页性能优化方式之一。在含有很多图片的页面中,如果所有图片都加载出来了,而用户却只能看到可是窗口的那一部分图片数据,就会影响性能,造成不必要的请求。
2、方案
解决上述问题,项目中一般会使用图片的懒加载。在滚动屏幕之前,只加载可视区域的图片,可视区域之外的图片不会进行加载。这样就可以加快页面的加载速度,减少服务器的负载。懒加载一般常用在图片较多的长列表场景中。
3、懒加载的优点:
markdown
1. 提升页面渲染效率,避免大量图片加载造成页面卡顿;
2. 按需加载,减少无效的图片加载,节约网络资源。
4、懒加载的核心原理
markdown
1. 将图片的src属性置空或者将图片地址存放在自定义属性[data-xxx]中,阻止图片加载;
2. 判断这些图片是否在可视区域内。
offsetTop
scrollTop
clientHeight
intersectionObserver
3. 触发方式
假设我们判断图片在可视区域内并进行图片懒加载操作函数是 lazyLoad 。一般都是页面滚动时触发,监听 window 对象的 scroll 事件。如果不考虑浏览器的兼容性 ,IntersctionObserver这个 API 提供的功能则是对目标元素进行监听,是一个比较新的api。
4. 防抖/节流
一次懒加载就可以加载整个可视页面的图片,而我们滚动一个页面的距离会触发很多次滚动事件,所以需要进行节流处理(当我们采用window.onscroll 方式监听并触发懒加载),需要将懒加载函数套一层节流函数,来限制其触发次数。如果采用的是 IntersectionOberserver,他会逐个检测目标元素是否进入可视窗口内,只有在可视窗口中才会触发对应的加载。
5. 快速滚动到页面底部
如果不使用 IntersectionObserver 判断图片是否在可视区域内,仅考虑从上到下滚动式,图片超过可视区域底部就触发懒加载的话,有一种场景: 用户进入页面后快速滚动到页面底部,这样就会导致所有图片都在可视区域出现过,所以所有图片都被会加载,导致用户想看的页面底部的图片很久都加载不出来。
所以如果使用传统的计算方法判断图片是否可见,既要计算是否超过可视区域底部,也要计算是否滑过可视区域上面去了,再配合节流限制懒加载函数的触发频率,从而达到性能的优化。
二、懒加载实现方式
原生页面, 先把自定义属性 data-src 内容写成正常的 src 地址,
js
<div class="container" ref="container">
<ul>
<li>
<!-- 如果有占位图,可以将图片的src都设为占位图的路径 -->
<img data-src="./img/1.jpg" alt="懒加载1" />
<img data-src="./img/2.jpg" alt="懒加载2" />
<img data-src="./img/3.jpg" alt="懒加载3" />
<img data-src="./img/4.jpg" alt="懒加载4" />
<img data-src="./img/5.jpg" alt="懒加载5" />
...
</li>
</ul>
</div>
<script src="./lazyLoad.js"></script>
计算逻辑
图片顶部到文档顶部的距离 > 浏览器可视窗口高度 + 滚动条滚过的高度 图片的高度 + 图片顶部到文档顶部的距离 < 滚动条滚过的高度
待加载图片的高度:
图片不在可视窗口内的条件: 图片距离容器的距离 > 容器的clientHeight 图片距离容器的距离 < -图片的高度
参数整理 待加载图片的高度:img.clientHeight 图片距离容器的距离: 图片的top - 容器的top 容器可视窗口的高度:container clientHeight
方案一: offsetTop,clientHeight,
未加防抖/节流的代码如下
js
// 获取到所有的 img 标签对应的元素,并存到 imgs 数组中
const imgs = document.getElementsByTagName('img'),
container = document.getElementById('container');
function lazyLoad(imgs) {
// 容器可视窗口的高度
let containerH = container.clientHeight,
containerT = container.offsetTop;
for (let i = 0; i < imgs.length; i++) {
// 根据之前分析的理论进行在窗口内的判断逻辑
// 图片距离容器的距离
let disTop = imgs[i].offsetTop - containerT;
if (disTop < containerH && disTop > -imgs[i].offsetHeight && !imgs[i].src) {
// 使用data-xxx 的自定义属性可以通过 dom 元素的 dataset.xxx 取值
imgs[i].src = imgs[i].dataset.src;
}
}
}
// 进入页面时执行一次加载
lazyLoad(imgs);
// 监听滚动事件,进行图片懒加载
window.onscroll = () => lazyLoad(imgs)
节流函数处理
js
/**
* @description: 节流函数
* @param { Function } fn 回调函数
* @param { Number } delay 间隔时间
* @param { ...any } args 回调函数 fn 需要用到的参数
* @return {*}
*/
// 节流处理
function throttle(fn,delay,...args) {
let timer = null;
return () => {
const context = this; // 保存当前上下文
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args); // 执行函数,并传入参数
timer = null; 清空定时器
},delay)
}
}
}
// 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)
window.onscroll = throttle(lazyLoad,500,imgs)
整合代码
js
// 获取到所有的 img 标签对应的元素,并存到 imgs 数组中
const imgs = document.getElementsByTagName('img'),
container = document.getElementById('container');
function lazyLoad(imgs) {
// 容器可视窗口的高度
let containerH = container.clientHeight,
containerT = container.offsetTop;
for (let i = 0; i < imgs.length; i++) {
// 根据之前分析的理论进行在窗口内的判断逻辑
// 图片距离容器的距离
let disTop = imgs[i].offsetTop - containerT;
if (disTop < containerH && disTop > -imgs[i].offsetHeight && !imgs[i].src) {
// 使用data-xxx 的自定义属性可以通过 dom 元素的 dataset.xxx 取值
imgs[i].src = imgs[i].dataset.src;
}
}
}
// 进入页面时执行一次加载
lazyLoad(imgs);
// // 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)
/**
* @description: 节流函数
* @param { Function } fn 回调函数
* @param { Number } delay 间隔时间
* @param { ...any } args 回调函数 fn 需要用到的参数
* @return {*}
*/
// 节流处理
function throttle(fn,delay,...args) {
let timer = null;
return () => {
const context = this; // 保存当前上下文
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args); // 执行函数,并传入参数
timer = null; 清空定时器
},delay)
}
}
}
// 监听滚动事件,进行图片懒加载
// window.onscroll = () => lazyLoad(imgs)
window.onscroll = throttle(lazyLoad,500,imgs)
方案二:getBoundingClientRect()
Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置
所以我们只要判断 元素相对于可视窗口顶部的距离 < 可视窗口高度 来确保滚动条到了图片的位置,同事 元素相对于可视窗口的顶部距离 大于 负的元素本身的高度 来保证图片没有滚动到可视窗口上方去。
js
// 获取到所有的 img 标签对应的元素,存到 imgs 数组中
let imgs = document.getElementsByTagName('img'),
container = document.getElementById('container');
function lazyLoad(imgs) {
// 容器可视窗口高度
const containerH = container.offsetHeight,
containerT = container.offsetTop;
for (let i = 0; i < imgs.length; i++) {
let imgT = imgs[i].getBoundingClientRect().top,
imgH = imgs[i].getBoundingClientRect().bottom - imgs[i].getBoundingClientRect().top
divTop = imgT - containerT;
// !imgs[i].src 当该图片已经加载好之后,无需重新加载
if (divTop < containerH && divTop > -imgH && !imgs[i].src) {
// 使用data-xxx 自定义属性 可以通过dom 元素的 dataset.xxx 取得。
imgs[i].src = imgs[i].dataset.src;
}
}
}
// 进入页面,先执行一次
lazyLoad(imgs);
function throttle(fn, delay, ...args) {
let timer = null;
return () => {
const context = this;
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args) // 执行函数,并传入参数
timer = null; // 清空定时器
},delay)
}
}
}
// 监听滚动事件,加载后面的图片
window.onscroll = throttle(lazyLoad,500,imgs)
方案三: IntersectionObserver
IntersectionObserver 是专门检测某个元素是否出现在可视窗口中
- 用法
js
const observer = new IntersectionObserver(callback[, options]);
以 IntersectionObserver 构造函数新建一个对象,接收两个参数 callback 和 options . 2. 实例方法 observe() 将一个元素加入监听目标集合 unobserve() 将一个元素一处监听目标集合 3. callback 当监听目标发生滚动变化时触发的回调函数。该回调函数接收一个参数 entries , 他其实就是被监听的元素与根元素容器的交叉状态。而每一个entry 有一个 target 属性,指向这个被监听的元素,有一个isIntersecting 属性,判断元素是否出现在视口内。同时也进行了节流处理,无需我们自己在进行手动节流处理
js
document.addEventListener('DOMContentLoaded', () => {
if ('IntersectionObserver' in window) {
const imgs = document.getElementsByTagName('img');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 通过该属性判断元素是否出现在视口内
// isIntersecting 布尔值,目标元素与交集观察者的根节点是否相交
if (entry.isIntersecting) {
// entry.target 能够获取到那个 dom 元素
// 被观察的目标元素,是一个DOM节点对象
let img = entry.target;
img.src = img.dataset.src;
// 图片加载完成后解除监听
observer.unobserve(img)
}
})
});
[...imgs].forEach(img => {
// 将所有的图片假如监听
observer.observe(img);
})
} else {
alert('您的浏览器尚不支持IntersectionObserver')
}
})
方案四: vue中自定义懒加载
v-lazy + InterSectionObserver
自定义指令 ,可以通过 app.directive 来全局注册。我们可以在 src 目录下的 directives 文件夹中新增一个 lazyLoad.js 文件 存放我们自定义懒加载指令
js
const lazyLoad = {
// mounted 在绑定元素的父组件及它自己的所有子节点都挂载完成后调用
mounted(el,binding) {
// 如果有需要可以先设置src为 loading
// el.setAttribute('src','loading 图的路径')
const options = {
threshold: [0.1], // 他是数组,决定何时触发回调函数,0.1 即交叉比例为10%时触发
rootMargin: '0px', // 用来扩大或者缩小视图的大小,使用 css 的定义方法,10px 10px 30px 20px 表示 top right bottom 和 left的值
// root: null, // 用于观察的根元素,默认是浏览器的视口,也可以指定具体元素,指定元素的时候用于观察的元素必须是指定元素的子元素
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// binding 是一个对象, binding.value 是传递给指令的值
el.setAttribute('src', binding.value);
// 移除监听
observer.unobserve(el)
}
})
}, options);
// 监听
observer.observe(el);
}
}
export default lazyLoad;
在 main.js 文件中注册我们的 v-lazy 指令:
js
// ...
import lazyLoad from './directives/lazyload';
const app = createApp(App); // App 组件
app.directive('lazy', lazyLoad);
// ...
最后在组件中使用,由于自定义指令在 main.js 中引入,不会被打包编译,只有 src 目录下的文件才会被编译。故而在 'lazyLoad.js' 中是不能够使用相对路径的。 解决方案:
- 使用绝对路径
- 使用 require 引入路径(Webpack) 或者 getImageUrl 引入路径(vite)
js
<script setup>
// getImageUrl
const getImageUrl = (name) => {
return new URL(`./img/${name}`, import.meta.url).href
}
</script>
<template>
<div class="container" ref="container">
<ul>
<li>
<!-- 如果有占位图,可以将图片的src都设为占位图的路径 -->
<img v-lazy="getImageUrl('1.jpg')" alt="懒加载1" />
<img v-lazy="getImageUrl('2.jpg')" alt="懒加载2" />
<img v-lazy="getImageUrl('3.jpg')" alt="懒加载3" />
<img v-lazy="getImageUrl('4.jpg')" alt="懒加载4" />
<img v-lazy="getImageUrl('5.jpg')" alt="懒加载5" />
</li>
</ul>
</div>
</template>
v-lazy + getBoundingClientRect()
上面我们通过 v-lazy:[container] = 'image.cover' 来接管每个图片的原始地址,其中 container 为父容器的类名,在指令里判断图片是否在可视区会用到父容器,并且可以在 data 中动态指定。
另外,需要提前准备一张占位图,每个图片默认开始时使用占位图,地址('src/directives/lazy/img/cover-def.gif')
封装一个事件总线
js
import Vue from 'vue'
const bus = new Vue();
Vue.prototype.$bus = bus
export default bus
页面模版
js
<script>
import { getImages } from '@/api/images'
export default {
data () {
return {
imgs: []
}
},
async created () {
const { data } = await getImages();
this.imgs = Object.freeze(data.list);
loadImages(this.imgs)
},
mounted(){
this.$ref.container.addEventListener('scroll', () => {
this.$bus.$emit('isScroll',this.$ref.container)
})
}
}
</script>
<template>
<div class='container' ref="container">
<ul>
<li v-for="(image,idx) in images" :key="image.id">
<img v-lazy:[container]="image.cover" alt="" :data-id="idx">
</li>
</ul>
</div>
</template>
<style scoped lang="less">
</style>
自定义指令
js
import Vue from 'vue'
import { debounce } from '@/utils'
import defImage from './img/cover-def.gif'
import eventBus from '@/eventBus'
// 用来收集需要懒加载的图片信息
let imgs = [];
const lazyLoad = {
mounted(el, binding) {
const { value, arg } = binding;
let container = el.parentNode
// 设置默认图片
el.src = defImage
// 找到容器
while (container && container.className.indexOf(arg) == -1) {
container = container.parentNode
}
// options
const img = {
el,
src: value,
container: {
top: container && container.getBoundingClientRect().top || 0,
clientHeight: container && container.clientHeight || 0
},
height: el.height || 0
}
// 添加到当前需要被懒加载的图片集合内,当㚵时,通过加载真实图片
imgs.push(img)
// 进入页面后立即执行一次
loadImg(img);
},
unmounted(el,binding) {
// 当前图片的指令接触绑定时,需要从imgs中删除(因为此时当前图片已经销毁)
deleteImg(el);
},
}
// 查看所有待加载的图片
function loadImages() {
for (const img of imgs) {
loadImg(img)
}
}
// 加载真实图片
function loadImg(img) {
const { el, src, height, container } = img;
img.top = el.getBoundingClientRect().top || 0;
const disTop = img.top - container.top;
// 不在可视区 或者 el.src!=defImage 已经加载过了
if (disTop > container.clientHeight && disTop < -height || el.src != defImage) return
// 在可视区
const image = new Image();
image.onload = () => {
el.src = src
}
image.src = src
// 加载过的图片,需要将其从imgs中清除
// deleteImg(el)
}
function deleteImg(el) {
// 删除imgs 中已经加载的真实图片
imgs = imgs.filter((img)=> img.el != el)
}
// 监听滚动
eventBus.$on('isScroll', debounce(loadImages, 500))
Vue.directive('lazy', lazyLoad)
export default lazyLoad;