今天学习了动画懒加载,IntersectionObserver是经典的主流方案,它是浏览器的原生API,采用异步执行,不会阻塞主线程。通过entry.isIntersecting 判断被观察的元素是否与视口(或根元素)相交,为true则触发动画,实现懒加载的效果。
一、JS原生实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>动画懒加载示例</title>
<style>
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideRight {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes bounceInTopLeft {
0% {
opacity: 0;
transform: translate(-100%, -100%) scale(0.3);
}
50% {
opacity: 1;
transform: translate(0, 0) scale(1.05);
}
70% {
opacity: 1;
transform: translate(0, 0) scale(0.95);
}
100% {
opacity: 1;
transform: translate(0, 0) scale(1);
}
}
.slide-left {
animation: slideLeft 0.6s ease-out forwards;
}
.slide-right {
animation: slideRight 0.6s ease-out forwards;
}
.slide-up {
animation: slideUp 0.6s ease-out forwards;
}
.bounce-in-left {
animation: bounceInTopLeft 2s cubic-bezier(0.68, -0.55, 0.265, 1.55)
forwards;
}
.ani-hid {
opacity: 0;
}
.right {
animation-delay: 1000ms;
}
.placeholder {
height: 100vh;
background: linear-gradient(to bottom, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.placeholder h1 {
color: white;
font-size: 2rem;
}
.left-right-wrap {
padding: 40px;
display: flex;
justify-content: space-between;
gap: 2.5rem;
}
.left-right-wrap .left,
.left-right-wrap .right {
flex: 1;
height: 300px;
border: 1px solid #ad1010;
}
.btn-wrap {
padding: 0 40px;
}
</style>
</head>
<body>
<div class="placeholder">
<h1>占位内容 - 滚动查看动画效果</h1>
</div>
<div class="left-right-wrap">
<div class="left ani-hid" data-animation="slide-left"></div>
<div class="right ani-hid" data-animation="bounce-in-left"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const element = entry.target;
const animationName = element.dataset.animation || 'slide-up';
// 添加动画类
element.classList.add(animationName);
// 只执行一次,停止观察已激活的元素
observer.unobserve(element);
}
});
},
{
threshold: 0.1, // 当元素10%可见时触发
rootMargin: '0px 0px -50px 0px', // 提前50px触发
}
);
// 观察所有包含 data-animation 属性的元素
const animatedElements = document.querySelectorAll('[data-animation]');
animatedElements.forEach((el) => {
observer.observe(el);
});
});
</script>
</body>
</html>
二、潜在问题
首屏可见元素或接近底部的元素可能永远不会触发 isIntersecting: true,这就导致元素动画类不会添加上。
1.首屏
2.底部
在body标签内底部添加按钮,效果是元素存在不显示(opacity:0):
<div class="btn-wrap">
<button class="ani-hid" data-animation="slide-up">
如果此按钮在最底部,不会显示
</button>
</div>
应该是与rootMargin设置有关,解决办法是设置底部padding/margin,值需要测试,与rootMargin值的大小相关:
<div class="btn-wrap" style="margin-bottom: 30px">
<button class="ani-hid" data-animation="slide-up">
如果此按钮在最底部,会显示
</button>
</div>
三、拓展
1.动画多参数配置
2.动态内容使用 MutationObserver + IntersectionObserver 组合
// 监听 DOM 变化,对新元素应用 IntersectionObserver
const mutationObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList.contains('lazy-animate')) {
intersectionObserver.observe(node);
}
});
});
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
四、框架下的封装
1.Vue:useIntersectionObserver
基于 IntersectionObserver 的 Vue 封装
import { useIntersectionObserver } from '@vueuse/core';
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }], observerElement) => {
if (isIntersecting) {
// 触发动画
stop(); // 可选的停止观察
}
},
)
2.React:Intersection Observer Hook
基于 IntersectionObserver,封装了 React 生命周期
import { useInView } from 'react-intersection-observer';
function MyComponent() {
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true
});
return <div ref={ref} className={inView ? 'animate-in' : ''} />;
}