给博客加动效的时候,考虑过用现成的动画库,最后还是决定用原生 JS 写。原因有三个:动效就那么几种,用不着一个几十 KB 的依赖;不引入构建步骤,主题文件改完直接生效;以及最重要的是加载速度和可控性。
这里记录几个实际用到的做法和其中的注意点。
一、滚动相关的都走 requestAnimationFrame
最常见的错误是直接在 scroll 事件里算位置、改样式。滚动事件触发非常频繁,一帧里可能触发很多次,每次都去读 getBoundingClientRect 就会强制浏览器反复重排。
简单的节流写法就够了:
var ticking = false;
function onScroll(fn) {
window.addEventListener('scroll', function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(function () {
ticking = false;
fn();
});
}, { passive: true });
}
关键是两点:用 requestAnimationFrame 把工作压到一帧只做一次;加上 { passive: true }——告诉浏览器这个监听器不会调用 preventDefault(),浏览器就不用等它执行完才开始滚动。
二、视差:只改 transform
首页大图的视差就是滚动时给它一个位移:
var shift = Math.min(y * 0.24, media.parentNode.offsetHeight * 0.1);
media.style.transform = 'translate3d(0,' + shift.toFixed(1) + 'px,0)';
有两点要注意。translate3d 而不是 top——transform 的动画可以由 GPU 合成,不会触发重排。Math.min 那个上限是必要的,否则滚动到页面底部时图片会移出容器,露出上方的空白。
三、淡入:交给 IntersectionObserver
元素进入视口时淡入,以前要自己算滚动位置,现在直接用 IntersectionObserver:
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add('is-in');
observer.unobserve(entry.target);
}
});
}, { rootMargin: '0px 0px -6% 0px', threshold: 0.04 });
Array.prototype.forEach.call(
document.querySelectorAll('.reveal'),
function (el) { observer.observe(el); }
);
动画本身还是 CSS 做的,JS 只负责加一个 class。这样做的好处是动画由浏览器自己调度,比用 JS 一帧一帧改样式流畅。
unobserve 那行别漏——元素已经出现过了就取消监听,不然列表长了会有几百个元素一直挂在观察器里。
想让列表里的卡片依次出现,用 CSS 变量传延迟就行,不用为每个元素写一条规则:
card.style.setProperty('--d', (i % 3) * 80 + 'ms');
.card { transition-delay: var(--d, 0ms); }
四、鼠标聚光:把坐标写进 CSS 变量
卡片跟随鼠标的高光效果,做法是把鼠标位置换算成百分比,写进自定义属性:
card.addEventListener('pointermove', function (e) {
var rect = card.getBoundingClientRect();
card.style.setProperty('--mx', ((e.clientX - rect.left) / rect.width * 100) + '%');
card.style.setProperty('--my', ((e.clientY - rect.top) / rect.height * 100) + '%');
});
CSS 那边读这两个变量画径向渐变:
.card__glow {
background: radial-gradient(
circle at var(--mx, 50%) var(--my, 50%),
rgba(66, 133, 244, .16), transparent 60%);
}
好处是 JS 只负责传数值,具体的视觉效果完全归 CSS 管,调样式不用碰 JS。
五、一定要尊重 prefers-reduced-motion
有些人系统里开着「减少动态效果」,可能是因为前庭功能的问题,动画会让ta们真的不舒服。所以每个动效前面都要加一道判断:
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) {
el.classList.add('is-in'); // 直接显示最终状态
} else {
observer.observe(el); // 交给观察器做动画
}
CSS 里也补一条兜底:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
transition-duration: .01ms !important;
}
}
六、小结
原生写动效的核心其实就三条:
- 滚动相关的工作压到
requestAnimationFrame里,一帧一次 - 能改
transform/opacity就不要改会引起重排的属性 - 把动画交给 CSS,JS 只负责切换状态和传参数
做到这三点,不引入任何库也能做出挺顺滑的效果,而且代码量比想象中少——上面这些加起来也就一百多行。
评论