by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12312
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
一、神奇效果抢先看
去年9月有介绍过CSS @function自定义函数,那么有没有可能自定义一个名为repeat(--n, anything)的函数,实现字符循环拼接或者数值相加的功能呢?
还真可以,例如:
<div class="target"></div>
@function --repeat(--n, --x) {
/* 略 */
}
.target:before {
--repeat-demo: calc(0 --repeat(16, + 10));
width: calc(var(--repeat-demo) * 1px);
counter-reset: result var(--repeat-demo);
content: counter(result) "px" --repeat(3, " ");
display: block;
line-height: 30px;
background: deepskyblue;
}
此时,可以看到,尺寸的160px生效了,result计数器的数值也生效了,以及--repeat(3, " ")的字符串拼接也生效了。
如下图所示:

眼见为实,您可以狠狠地点击这里:CSS自定义任意可重复可相加函数demo
所以,@function --repeat(--n, --x)这个自定义函数里面究竟是怎么设置的,居然可以有这样的效果呢?
二、解密–repate()函数代码
此自定义CSS函数代码是固定的,粘贴即可使用:
@function --repeat(--n, --x) {
--bit7: calc(round(down, var(--n) / 128));
--val6: calc(var(--n) - var(--bit7) * 128);
--bit6: calc(round(down, var(--val6) / 64));
--val5: calc(var(--val6) - var(--bit6) * 64);
--bit5: calc(round(down, var(--val5) / 32));
--val4: calc(var(--val5) - var(--bit5) * 32);
--bit4: calc(round(down, var(--val4) / 16));
--val3: calc(var(--val4) - var(--bit4) * 16);
--bit3: calc(round(down, var(--val3) / 8));
--val2: calc(var(--val3) - var(--bit3) * 8);
--bit2: calc(round(down, var(--val2) / 4));
--val1: calc(var(--val2) - var(--bit2) * 4);
--bit1: calc(round(down, var(--val1) / 2));
--bit0: calc(var(--val1) - var(--bit1) * 2);
--pow0: var(--x);
--pow1: var(--pow0) var(--pow0);
--pow2: var(--pow1) var(--pow1);
--pow3: var(--pow2) var(--pow2);
--pow4: var(--pow3) var(--pow3);
--pow5: var(--pow4) var(--pow4);
--pow6: var(--pow5) var(--pow5);
--pow7: var(--pow6) var(--pow6);
result: if(style(--bit0 = 1): var(--pow0); else: ;)
if(style(--bit1 = 1): var(--pow1); else: ;)
if(style(--bit2 = 1): var(--pow2); else: ;)
if(style(--bit3 = 1): var(--pow3); else: ;)
if(style(--bit4 = 1): var(--pow4); else: ;)
if(style(--bit5 = 1): var(--pow5); else: ;)
if(style(--bit6 = 1): var(--pow6); else: ;)
if(style(--bit7 = 1): var(--pow7); else: ;);
}
或者直接@import在线的CSS地址。
@import url("https://unpkg.com/@propjockey/doubledash.css@0.0.2/functions/repeat.css");
案例
1. 字符串循环拼接
@import url("./repeat.css");
body::before {
content: --repeat(7, " ");
}
实时渲染效果如下:
2. text-shadow文字阴影
@import url("https://unpkg.com/@propjockey/doubledash.css@0.0.2/functions/repeat.css");
@import url("https://unpkg.com/@propjockey/doubledash.css@0.0.2/functions/repeat-join.css");
body::before {
content: --repeat(7, "CSS新世界 ");
text-shadow: --repeat-join(4, {, }, 0px 0px 2px deeppink);
}
实时渲染效果如下:
3. 使用drop-shadow()滤镜代替
@import url("./repeat.css");
body::before {
content: --repeat(3, "CSS新世界 ");
filter: --repeat(4,
drop-shadow(1px 1px deeppink)
);
font-weight: bold;
color: white;
font-family: system-ui;
}
实时渲染效果如下:
4. 在shape()函数中使用repeat
@import url("repeat.css");
@import url("repeat-join.css");
body::before {
content: --repeat(3, "HTML并不简单 ");
display: inline-block;
border-shape: shape(
from 0 30px,
--repeat-join(255, {,}, {line by 5px -6px, line by 5px 6px})
);
height: 36px;
border: 1px solid deeppink;
clip-path: inset(0px);
}
实时渲染效果如下所示:
二、原理说明
这是一个非常巧妙的 CSS @function,核心思想是用二进制分解 + 倍增(快速幂思想)来实现”重复 N 次”。下面拆解原理。
1. 目标
--repeat(--n, --x) 要返回:把 --x 这个 token(可以是字符串片段、也可以是某个属性值单元)平铺重复 --n 次,中间用空格连接。
比如 --repeat(5, a) → a a a a a(5 个 a)。
难点在于:CSS 没有循环,无法写 for (i=0; i<n; i++)。于是作者用了二进制展开这个数学技巧。
2. 第一部分:把 N 拆成二进制位
--bit7: calc(round(down, var(--n) / 128));
--val6: calc(var(--n) - var(--bit7) * 128);
--bit6: calc(round(down, var(--val6) / 64));
...
--bit0: calc(var(--val1) - var(--bit1) * 2);
这段就是十进制转二进制的标准”逐位提取”算法。round(down, …) 是向下取整(等价于 floor)。
以 --n = 5 为例(二进制 00000101):
| 步骤 | 计算 | 结果 |
|---|---|---|
| bit7 | floor(5/128) | 0,余 5 |
| bit6 | floor(5/64) | 0,余 5 |
| bit5 | floor(5/32) | 0,余 5 |
| bit4 | floor(5/16) | 0,余 5 |
| bit3 | floor(5/8) | 0,余 5 |
| bit2 | floor(5/4) | 1,余 1 |
| bit1 | floor(1/2) | 0,余 1 |
| bit0 | 1 – 0 | 1 |
所以 --bit2 = 1、--bit0 = 1,其余为 0 → 对应 5 = 4 + 1 = 2² + 2⁰。
由于只到 bit7(128),这个函数支持的最大 N 是 255(2⁸ - 1)。
3. 第二部分:倍增构造 2ⁿ 个副本(快速幂)
--pow0: var(--x); /* 1 个 x (2^0) */
--pow1: var(--pow0) var(--pow0); /* 2 个 x (2^1) */
--pow2: var(--pow1) var(--pow1); /* 4 个 x (2^2) */
--pow3: var(--pow2) var(--pow2); /* 8 个 x (2^3) */
...
--pow7: var(--pow6) var(--pow6); /* 128 个 x (2^7) */
这是关键的倍增(doubling)技巧:每一级都把上一级的内容拼两遍,于是 --powK 恰好包含 2^K 个 --x,用空格分隔。
注意这里不需要循环也不需要条件——纯粹靠变量自引用的层层展开,--pow7 展开后就是实实在在的 128 个 token 并排。
这一步类似”生成 8 个预制的积木块,长度分别是 1、2、4、8、16、32、64、128″。
4. 第三部分:按二进制位选择性拼接
result: if(style(--bit0 = 1): var(--pow0); else: ;)
if(style(--bit1 = 1): var(--pow1); else: ;)
...
if(style(--bit7 = 1): var(--pow7); else: ;);
if(style(--bitK = 1): …; else: ;) 是新的 CSS 内联条件(if() 函数 + style() 查询):
- 如果第 K 位是 1,就取出对应的积木块
--powK(含2^K个 x); - 否则输出空(
else:后面是空的)。
把 8 个条件的结果首尾相接,就得到总数为 Σ (bitK × 2^K) 个 x —— 这正好等于 --n。
以 --n = 5(bit2=1, bit0=1)为例:
- bit0=1 → 输出
--pow0= 1 个 x - bit1=0 → 空
- bit2=1 → 输出
--pow2= 4 个 x - 其余 → 空
- 拼接 =
4 + 1 = 5个 x ✅
5. 整体原理一句话总结
把”重复 N 次”这个无法用循环表达的操作,转化为”N 的二进制分解”问题:先用倍增法预生成 1、2、4、…、128 份副本,再根据 N 的每一个二进制位决定是否把对应份数拼进结果,最终拼出的总份数恰好等于 N。
这本质上就是快速幂 / 二进制拆分思想在 CSS token 拼接上的应用,把原本需要 O(N) 次循环的重复,压缩成 O(log N) = 最多 8 个固定的条件判断。
几个值得注意的细节
-
为什么
--x可以是”字符串或属性值”:CSS 自定义属性存的是未解析的 token 序列,--x只是被当作一段 token 反复拼接,函数本身不关心它语义上是关键字、长度值还是字符串片段,所以既能拼grid-template-columns的1fr,也能拼平铺文本片段。最终由使用方的属性去解释这串 token 的含义。 -
分隔符是空格:
var(--pow0) var(--pow0)之间天然有空格,if()各段之间也有空格。所以它做的是”空格分隔的平铺”,非常适合grid-template-columns: --repeat(10, 1fr)这类场景(等效于repeat(10, 1fr)展开)。 -
上限 255:只展开到
bit7 / pow7。想支持更大,再往上加bit8 / pow8(256)即可,按同样规律扩展。 -
依赖的新特性:
@function、if()+style()查询、round()数学函数——都属于较新的 CSS 规范(CSS Values 4 / CSS Functions),目前浏览器支持仍在铺开阶段,需注意兼容性。
四、结语说明
本文内容让我不由得想起4月份介绍的蜂巢布局效果,也就是金字塔布局:CSS六边形头像的实现与蜂巢布局。
其中,也用到了CSS if()函数和大量的数学计算。
不过,这种案例炫技的成分大一些,还不太适合用在生产环境。
实际开发,多半还是老老实实字符平铺写死,而不会使用自定义的 --repeat 函数。
好了,就说这么多吧,算是增长眼界的一篇文章了。
参考文章:
本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12312
(本篇完)