在 SVG 动画开发里,文本不像位图那样只能整体替换,它是以 <text> 或 <tspan> 节点存在于 DOM 树中的真实元素。很多同学第一次做进度环动画时,希望中间的百分比数字跟着圆环一起增长,却在循环播放后发现文字永远停在初始值。这是因为 SVG 的声明式动画(如 SMIL 的 <animate>)默认只驱动属性变化,不会触碰子文本节点。要动态修改,就必须从 DOM 层面入手,用脚本在合适的时机写入新内容。

一、使用 JavaScript 直接操作 textContent
最直观的方案是通过 document.querySelector 拿到 <text> 元素,然后改写它的 textContent 属性。由于 SVG 文本节点的文本内容就是其直接子文本,赋值操作会立刻触发浏览器重新排版并渲染新字形。这种方式不依赖任何外部库,在原生 Web 环境或框架的挂载完成后都能用。
下面示例展示了一个简单的圆形进度,数字从 0 走到 100。我们用 requestAnimationFrame 自己算帧,每帧重设 <text> 里的字符串。注意 SVG 坐标系统里文字默认基线对齐,通常用 text-anchor="middle" 配合 dominant-baseline="central" 让它居中。
<svg width="200" height="200" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="80" fill="none" stroke="#eee" stroke-width="10"/>
<circle id="bar" cx="100" cy="100" r="80" fill="none" stroke="#3b9" stroke-width="10"
stroke-dasharray="502" stroke-dashoffset="502"/>
<text id="label" x="100" y="100" text-anchor="middle" dominant-baseline="central"
font-size="28" fill="#333">0%</text>
</svg>
<script>
const bar = document.getElementById('bar');
const label = document.getElementById('label');
const total = 502;
let start = null;
function step(ts){
if(!start) start = ts;
let p = Math.min((ts - start) / 2000, 1);
bar.setAttribute('stroke-dashoffset', total * (1 - p));
label.textContent = Math.round(p * 100) + '%';
if(p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
</script>
这种写法的好处是逻辑完全透明,你想在什么时间点改、改成什么格式都由自己控制。缺点在于如果页面上有几十条这类动画,频繁写 textContent 可能引起不小的中文重绘开销,尤其在低端设备上会掉帧。此时可以只更新变化的位,而不是整段重写。
二、借助 tspan 分段降低重绘范围
当文本结构复杂,比如“当前温度 23℃”中只有数字在变,可以把固定部分和变化部分拆进不同的 <tspan>。脚本只改数字所在的 <tspan> 的 textContent,浏览器只需重绘那一小段字形,布局树其余部分不动。对于频繁刷新的仪表盘,这种细粒度更新能明显减少回流。
下面的代码把单位与数值分离,动画过程里仅改写 val 这个 <tspan>。从 DOM 角度看,父 <text> 的子节点结构保持稳定,只有其中一个文本叶节点被替换,渲染引擎处理起来更轻。
<svg width="240" height="80" viewBox="0 0 240 80">
<text x="20" y="45" font-size="22" fill="#444">
当前温度 <tspan id="val" fill="#e60">0</tspan>℃
</text>
</svg>
<script>
const val = document.getElementById('val');
let n = 0;
setInterval(() => {
n = (n + 1) % 50;
val.textContent = n;
}, 800);
</script>
使用 <tspan> 还要注意继承问题:如果外层 <text> 设了 font-family,内部 <tspan> 未覆盖就会继承,不会出现样式跳变。但一旦你用脚本整体替换掉 <text> 的 innerHTML,就可能丢掉原有的 <tspan> 结构,反而更慢。因此分段思路的核心是“改叶不改枝”。
三、在 SMIL 或 CSS 动画帧回调中同步文本
如果你偏好声明式动画,比如用 SMIL 的 <animateTransform> 旋转指针,或者 CSS @keyframes 做颜色渐变,仍然可以用 JS 监听 timeupdate 类事件或 animationiteration 来同步文本。SMIL 本身不支持文本插值,但我们可以监听 beginEvent 与 endEvent,在阶段切换时写入对应文案,实现“步骤式”动态文本。
下面例子用 CSS 动画驱动一个提示框的透明度,并在动画每次迭代时更换 <text> 里的语句。这样做让视觉节奏交给样式表,文字逻辑交给脚本,职责分离清晰,也方便后期让设计同学只改 CSS 而不动 JS。
<svg width="300" height="60">
<text id="tip" x="10" y="35" font-size="18">正在连接服务器…</text>
</svg>
<style>
#tip { animation: blink 1.2s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
</style>
<script>
const tips = ['正在连接服务器…','校验用户身份…','拉取配置数据…','准备就绪'];
let i = 0;
const tip = document.getElementById('tip');
tip.addEventListener('animationiteration', () => {
i = (i + 1) % tips.length;
tip.textContent = tips[i];
});
</script>
从工程维护角度,把动画表现与数据更新解耦是长久之计。若项目已用 GSAP 等库,其 onUpdate 回调里直接写 label.textContent 即可,原理和我们上面原生写法一致,只是时间轴更精准。无论哪种路线,核心都在于认清 SVG 文本是可脚本化的 DOM 节点,而不是静态图片的一部分。
SVG动画动态文本JavaScript修改时间:2026-08-19 01:32:31