How Vue3 loads images elegantly
Recently, a function was developed. The homepage of the page will load a large number of pictures. When entering the page for the first time, the performance of the page will be reduced.
Ever since, I improved this function so that all images can be loaded automatically and lazily.
principle
The main underlying logic of this function is IntersectionObserver
API
to IntersectionObserver
observe the visibility and position changes of elements in the browser. It can help developers implement some dynamic behaviors, such as lazy loading of images, infinite scrolling, etc.
A simple example is as follows:
// 创建IntersectionObserver实例
const observer = new IntersectionObserver((entries, observer) => {
// 遍历观察的元素
entries.forEach(entry => {
// 如果元素可见
if (entry.isIntersecting) {
// 加载图片
const img = entry.target;
const src = img.getAttribute('src');
img.setAttribute('src', src);
// 停止观察该元素
observer.unobserve(img);
}
});
});
// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('.lazy-image');
// 观察每个图片元素
lazyImages.forEach(image => {
observer.observe(image);
});
practice
Next we implement a general hook
, basic function as follows:
- Provide a default placeholder image for the image
src
and providesrc
attributes - Pass in the attributes corresponding to the image
ref
. - When the picture enters the visible area, use
src
the attribute to replacesrc
the attribute
import {
onMounted, Ref } from "vue";
const options = {
// root: document.querySelector(".container"), // 根元素,默认为视口
rootMargin: "0px", // 根元素的边距
threshold: 0.5, // 可见性比例阈值
once: true,
};
function callback(
entries: IntersectionObserverEntry[],
observer: IntersectionObserver
) {
entries.forEach((entry) => {
// 处理每个目标元素的可见性变化
if (entry.intersectionRatio <= 0) return;
const img: Element = entry.target;
const src = img.getAttribute("src");
img.setAttribute("src", src ?? ""); // 将真实的图片地址赋给 src 属性
observer.unobserve(img);
});
}
export const useInView = (ref: Ref) => {
const observer = new IntersectionObserver(callback, options);
onMounted(() => {
Object.keys(ref.value).forEach((e) => observer.observe(ref.value[e]));
});
};
<script setup lang="ts">
import { ref } from "vue";
import { useInView } from "./hooks/useInView";
const imgRef = ref(null);
useInView(imgRef);
</script>
<template>
<div
v-for="(_, idx) in new Array(200).fill(11)"
>
<img
ref="imgRef"
src="https://via.placeholder.com/200"
:src="`https://picsum.photos/200/${180 + idx}`"
alt="b"
/>
</div>
</template>
The actual effect is as follows
Although the basic functional requirements have been completed, it is not elegant enough yet! ! !
optimization
Next, we add a transition animation. Each time when the image is loaded, it transitions from the placeholder image to the normal image mode.
img.onload = () => {
img.setAttribute('class', 'fade-in')
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 应用淡入动画到元素 */
.fade-in {
animation: fadeIn 0.6s ease-in;
}
The complete code is as follows:
import {
onMounted, Ref } from "vue";
const options = {
// root: document.querySelector(".container"), // 根元素,默认为视口
rootMargin: "0px", // 根元素的边距
threshold: 0.5, // 可见性比例阈值
once: true,
};
function callback(
entries: IntersectionObserverEntry[],
observer: IntersectionObserver
) {
entries.forEach((entry) => {
if (entry.intersectionRatio <= 0) return;
const img = entry.target as HTMLImageElement;
const src = img.getAttribute("src");
img.setAttribute("src", src ?? ""); // 将真实的图片地址赋给 src 属性
img.onload = () => {
img.setAttribute("class", "fade-in");
};
observer.unobserve(img);
});
}
export const useInView = (ref: Ref) => {
const observer = new IntersectionObserver(
callback,
options
);
onMounted(() => {
Object.keys(ref.value)
.forEach((e) => observer.observe(ref.value[e]));
});
};
<script setup lang="ts">
import { ref } from "vue";
import { useInView } from "./hooks/useInView";
const imgRef = ref(null);
useInView(imgRef);
</script>
<template>
<div
v-for="(_, idx) in new Array(200).fill(11)"
style="width: 200px height: 200px;"
>
<img
ref="imgRef"
style="height: 100%"
src="https://via.placeholder.com/200"
:src="`https://picsum.photos/200/${180 + idx}`"
alt="b"
/>
</div>
</template>
<style scoped>
/* 定义淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 应用淡入动画到元素 */
.fade-in {
animation: fadeIn 0.6s ease-in;
}
</style>