使用SCSS递归函数生成阶梯式延迟CSS加载动画
时间:2026-07-15 18:55
SCSS递归生成阶梯延迟动画需将递归转化为迭代,用@for生成延迟映射表,再用@each配合nth-child输出CSS规则。nth-child比nth-of-type更可靠,因严格按照DOM顺序计数。移动端iOSSafari在步数超过12时可能卡顿,建议控制步数并使用透明度与变换属性组合。
在实际项目中,使用SCSS模拟递归实际上是一个常见的误解。SCSS并不具备真正的运行时递归能力,其所谓的“递归”本质上是通过迭代展开的循环操作。如果试图编写类似 `@return my-function($n - 1)` 的逻辑,编译时必然会触发 `Stack overflow: maximum nesting level reached` 错误——这是语言本身的限制所致,而非代码不够“巧妙”。
正确的实现思路非常简单:将递归逻辑转换为迭代逻辑,借助 `@for` 循环从1到 `$steps` 逐层构建一个延迟map。该map仅作为数据准备阶段使用,不输出任何CSS规则。
```scss
@function generate-delays($steps, $base: 0.1s, $increment: 0.1s) {
$result: ();
@for $i from 1 through $steps {
$delay: $base + ($i - 1) * $increment;
$result: map-merge($result, (nth: $i, delay: $delay));
}
@return $result;
}
```
获得这个map之后,下一步才是真正的CSS输出环节。这里最容易踩的坑是:试图在 `@for` 循环中直接拼接选择器字符串,结果生成了无效的CSS规则(例如 `.item:nth-child(1)` 未被解析为真正的选择器)。正确的做法是使用 `@each` 配合 `nth-child()` 来展开。
下面这个mixin专门负责此项任务:
```scss
@mixin stagger-delay($steps, $base: 0.1s, $increment: 0.1s) {
$delays: generate-delays($steps, $base, $increment);
@each $key, $value in $delays {
@if type-of($key) == 'number' {
&:nth-child(#{$key}) {
animation-delay: $value;
}
}
}
}
```
调用方式非常直观:
```scss
.loader-item {
animation: fade-in 0.3s ease-out;
@include stagger-delay(5, 0.1s, 0.08s);
}
```
编译后生成的CSS如下:
```css
.loader-item:nth-child(1) { animation-delay: 0.1s; }
.loader-item:nth-child(2) { animation-delay: 0.18s; }
.loader-item:nth-child(3) { animation-delay: 0.26s; }
/* ... */
```
---
**为什么 `nth-child` 比 `nth-of-type` 更可靠?**
这里有一个关键陷阱需要留意:如果容器中混合了其他类型的元素(比如一个 `
` 或一个 ``),使用 `nth-of-type` 会导致延迟错位——它只统计相同标签类型的元素,而 `nth-child` 严格按照DOM顺序计数。动画序列依赖的是元素的渲染顺序,而非语义类型。
因此建议始终坚持以下原则:
- 所有目标元素必须是同一父容器下的直接子节点
- 不要用 `visibility: hidden` 来隐藏中间项——它仍然占据 `nth-child` 的位置,但动画不可见,很容易造成“跳帧”的错觉
- 如果列表是动态增删的,纯CSS方案无法胜任,此时应交给JavaScript来控制 `style.animationDelay`
---
**移动端iOS Safari的卡顿问题**
旧版iOS Safari对大量 `animation-delay` 叠加比较敏感。当 `$steps` 超过12时,可能会出现首帧卡顿或延迟完全不生效的现象。根本原因在于浏览器对多个 `nth-child` 规则的解析开销急剧增加。
可以尝试以下几种缓解措施:
- 将 `$steps` 控制在8以内,尽量使用 `opacity` 和 `transform` 组合代替复杂动画
- 对较长的列表,提前加上 `will-change: transform` 让浏览器预分配渲染层
- 避免在 `@keyframes` 中写入 `background-color` 渐变——重绘开销太大,优先使用 `opacity` 和 `scale`
说到底,CSS预处理器并非万能的解决方案。真正复杂的场景——比如瀑布流加载、滚动触发动画——不要硬扛SCSS生成,该交给JavaScript就交给JavaScript。明确工具的边界,否则反而会给自己挖坑。
