首页
/ d3 过渡选择与链式动画指南:深入 d3-transition 的选择、同步与接力 API

d3 过渡选择与链式动画指南:深入 d3-transition 的选择、同步与接力 API

2026-09-06 13:58:01作者:伍霜盼Ellen

本篇技术指南围绕 d3 v7(当前仓库版本 7.9.0)官方文档 docs/d3-transition/selecting.md 展开,系统讲解如何在 d3-selection 之上创建、子选择、合并与接力过渡(transition)。读完后你可以掌握:如何用 *selection*.transition(name) 在不同元素集合间同步同一个过渡,如何用 transition.select/selectAll/filter/merge 在过渡上做子选择,以及如何用 transition.transition()d3.active() 编排链式、循环动画。

1. 创建过渡:selection.transition(name)

过渡是"selection-like"(类似选择集)的接口:不立即修改 DOM,而是在给定时长内将 DOM 从当前状态平滑插值到目标状态(参见 d3-transition 总览)。所有过渡都派生自选择集,入口就是 *selection*.transition(name)

  • 参数 name 为过渡名,不传时默认使用 null
  • 返回一个新过渡,作用于当前选择集上的每个元素;
  • 排他性规则:新过渡只与"同名"的其他过渡互斥。即同一元素上可以同时存在 null 名过渡和 "update" 名过渡而互不干扰。

原文文档给出的典型场景是跨选择集同步同一个过渡:当 name 传入的是一个过渡实例(而非字符串)时,返回的过渡与该实例具有相同的 id 和 name;若某个被选元素上已存在同 id 的过渡,则对该元素直接返回已存在的过渡;否则,返回过渡的定时(timing)会从"每个被选元素最近的祖先上同 id 的既有过渡"继承而来。这样你就可以先定义一个过渡模板,再把不同的选择集"挂"到同一时间轴上:

const t = d3.transition()
    .duration(750)
    .ease(d3.easeLinear);

d3.selectAll(".apple").transition(t)
    .style("fill", "red");

d3.selectAll(".orange").transition(t)
    .style("fill", "orange");

.apple.orange 两组元素由此共用同一过渡 id,起始时间保持一致,实现同步动画。

边界情况(原文档明确警告):如果指定的过渡实例既不在被选节点上、也不在其任何祖先上(例如该过渡已经结束),当前会回退到默认定时参数;但原文档指出,未来版本可能会改为抛出错误(对应 d3-transition 上游 issue #59 的行为讨论)。因此在生产代码中不要依赖"找不到模板过渡时静默回退"这一行为。

默认定时参数

结合 Timing 文档 可以确认,创建过渡后可用以下方法覆盖默认值:

方法 默认值 说明
*transition*.delay(value) 0 ms 延迟开始的时间(毫秒),支持逐元素函数
*transition*.duration(value) 250 ms 持续时间(毫秒),支持逐元素函数
*transition*.ease(value) d3.easeCubic 缓动函数,每帧传入归一化时间 t∈[0,1]

这三个方法都接受"常量或函数":函数形式会被立即对每个被选元素求值,依次传入当前数据 d、当前索引 i、当前组 nodesthis 为当前 DOM 元素。

2. 过渡级选择函数:d3.transition(name)

d3.transition(name) 直接在文档根元素 document.documentElement 上创建一个新过渡,name 的语义与 *selection*.transition 完全一致(不传为 null,只与同名过渡互斥;也可传入过渡实例,见上一节)。它等价于:

d3.selection()
  .transition(name)

此外,d3.transition 还承担两个工程用途:

  1. 类型判断:可用 t instanceof d3.transition 测试一个值是否为过渡实例,这在前文 selection.transition(t) 接收过渡实例时非常有用;
  2. 扩展原型:由于过渡对象基于原型链构造,你可以通过修改 d3.transition.prototype 为所有过渡添加自定义方法。

3. 过渡的子选择:select / selectAll / selectChild / selectChildren / filter

这组方法是 d3-transition 独有的"过渡版子选择",它们在过渡上而不是在选择集上工作,因此返回的是过渡而非选择集,可以直接继续链式设置 styleattr 等。

transition.select(selector)

对当前过渡的每个被选元素,选出第一个匹配 selector 的后代元素(如有),并在新选择上返回过渡。等价于"先取选择、再做子选择、最后按同一过渡 id 派生新过渡"这三步的组合:

transition
  .selection()
  .select(selector)
  .transition(transition)

transition.selectAll(selector)

对每个被选元素,选出所有匹配的后代元素。同样等价于:

transition
  .selection()
  .selectAll(selector)
  .transition(transition)

transition.selectChild(selector) / transition.selectChildren(selector)

这两个是 d3 v7 新增的"仅子元素"变体:selectChild 对每个被选元素选出第一个匹配的直接子元素selectChildren 选出所有匹配的直接子元素。与 select/selectAll 的区别在于不再向下穿透更深层后代。分别等价于:

transition
  .selection()
  .selectChild(selector)
  .transition(transition)
transition
  .selection()
  .selectChildren(selector)
  .transition(transition)

对应底层语义可参考 d3-selection 的子选择文档:例如 selectChild 会把父元素的数据传播(propagate)到选中的子元素上,这对"外层 g 绑定数据、内层子元素做动画"的 SVG 结构尤其重要。

参数与排他性(四个子选择方法通用)

  • selector 可以是选择器字符串,也可以是函数;函数按序对每个被选元素求值,传入 dinodesthis 为当前 DOM 元素;
  • 新过渡与本过渡具有相同的 id、name 和 timing,因此父过渡跑到一半时,对其子元素调用 transition.select(".tick").style(...),子过渡会在同一 id 下被继承并同步;
  • 若目标元素上已存在同 id 的过渡,则对该元素返回既有过渡——这一点保证了"重复子选择不会重启动画",是构建增量动画的关键幂等性。

transition.filter(filter)

只保留匹配 filter 的元素,返回子集上的过渡。filter 为选择器字符串或函数(函数参数同样是 dinodesthis 为当前 DOM 元素)。等价于:

transition
  .selection()
  .filter(filter)
  .transition(transition)

实际用途典型如:数据更新时先对整组元素启动一个过渡,再 filter(d => d.value > 0) 只对满足条件的子集追加不同的 attr 目标,其余元素保持原过渡不变。

4. 过渡合并:transition.merge(other)

返回本过渡与 other 合并后的新过渡,要求 other 与本过渡具有相同的 id。结果过渡保持与本体相同的组数、parent、name 和 id;本过渡中缺失(null)的位置,会用 other 中对应位置的非 null 元素补上。

等价于在 selection 层面合并后再派生过渡:

transition
  .selection()
  .merge(other.selection())
  .transition(transition)

这与 selection.merge 的语义一致:两个"组结构相同、部分元素互补"的过渡(例如 enter 组与 update 组分开创建的过渡)可以先合并,再统一调用 transition.attr("cx", ...),避免对两个过渡重复写同一目标值。

5. 链式过渡:transition.transition()

在本过渡结束时,对相同的被选元素调度一个新的过渡。新过渡继承:

  • 参考时间:等于本过渡的时间加上它的 delayduration
  • 本过渡的 name、duration 与 easing

官方文档示例展示了"绿色 → 红色 → 停顿 1 秒 → 棕色并移除"的多段接力:

d3.selectAll(".apple")
  .transition() // First fade to green.
    .style("fill", "green")
  .transition() // Then red.
    .style("fill", "red")
  .transition() // Wait one second. Then brown, and remove.
    .delay(1000)
    .style("fill", "brown")
    .remove();

关键语义:每一段的 delay 都是相对于上一段过渡结束时刻的偏移。所以上例中苹果保持红色 1 秒后,才会开始向棕色过渡——这是"相对时间轴"而非绝对时间戳,编写多阶段动画时务必按此理解。

注意与第 3 节的配合:链上每一段的 transition.transition() 都继承同一 id,而 *transition*.remove()(见 modifying 文档)会在过渡结束时移除元素,因此链式过渡是"先动画、后清理"的标准写法。

6. 取出选择集与查询活跃过渡

transition.selection()

返回该过渡对应的 selection。这是"过渡世界"回到"选择世界"的桥:当你需要在过渡之外操作元素(例如 append 新元素——过渡不支持创建元素,必须先 append 或绑定数据再启动过渡,见 d3-transition 总览),用 transition.selection().append("circle") 即可。

active(node, name)

返回指定 node 上名为 name活跃过渡(如存在);不传 name 时按 null 查;没有则返回 null。文档标注其典型用途是"创建链式过渡"。官方"迪斯科模式"示例展示了如何借事件回调自举出无限循环动画:

d3.selectAll("circle").transition()
    .delay((d, i) => i * 50)
    .on("start", function repeat() {
        d3.active(this)
            .style("fill", "red")
          .transition()
            .style("fill", "green")
          .transition()
            .style("fill", "blue")
          .transition()
            .on("start", repeat);
      });

拆解其机制:d3.active(this) 取回当前正在运行的过渡,在其上再 .transition() 依次挂接红、绿、蓝三段接力过渡,最后一段的 on("start", repeat) 又注册同一个回调,从而每轮开始都递归重建下一轮——由于 transition.transition() 的相对 delay 语义,轮次之间无缝衔接。这里的 .on("start", ...)过渡事件start / end / interrupt / cancel),与原生 DOM 事件不同,详见 控制流文档

7. 源码与仓库结构印证

从当前仓库的包结构看,d3 元包并不内联实现过渡逻辑,而是聚合各独立子包:

  • src/index.js 第 29 行 export * from "d3-transition"; 将 d3-transition 的全部 API 并入 d3 命名空间,因此 d3.transitiond3.active 与各 *selection*.transition 行为都由 d3-transition 子包提供;
  • package.json 声明依赖 "d3-transition": "^3.0.1"(同时含 d3-selection": "^3.0.0"d3-timer": "^3.0.1"),与文档所述 v3 系列 API(如 selectChild/selectChildren 为 v7 新增)一致;
  • 原文档为每个 API 标注了 d3-transition 子包内的源码文件,如 selection.transition 对应子包 src/selection/transition.js*transition*.select 对应 src/transition/select.jsactive 对应 src/active.js 等(当前仓库未随附 node_modules,无法在本仓库内直接查看这些实现文件,此处仅转述文档标注的位置)。

由文档描述与 control-flow 中"过渡的一生" 可以推断出若干工程约束:

  1. 中断发生在 start 而非创建:新过渡启动时才会中断同元素同名活跃过渡并派发 interrupt 事件;零延迟过渡也不会立即中断旧过渡(旧过渡还有一帧),需要立即中断时应调用 *selection*.interrupt(name)
  2. 可修改窗口有限:过渡一旦调度(当前帧末或下一帧)便不能再改 delaystart 监听器(报错 "too late: already scheduled"),运行后连 timing、tween、监听器都不可改(报错 "too late: already running"),结束后配置即被销毁("transition not found")。因此第 5 节链式过渡中"延迟到运行时才能确定的起始值"类逻辑必须写进 on("start")attrTween/styleTween
  3. 同 id 幂等:第 1、3、4 节反复出现的"若元素上已有同 id 过渡则返回既有过渡",与上述"中断/取消同名过渡"的机制共同构成了 d3 过渡的冲突消解模型——这解释了为什么模板过渡同步(第 1 节)与幂等子选择(第 3 节)能够可靠工作。

8. API 速查

API 作用 关键约束
selection.transition(name) 在选择集上创建过渡;name 可为字符串或过渡实例 默认 name 为 null;仅与同名过渡互斥;找不到模板过渡时回退默认定时
d3.transition(name) document.documentElement 上创建过渡;instanceof 判断与原型扩展 等价于 d3.selection().transition(name)
transition.select(selector) 子选择:第一个匹配的后代 继承 id/name/timing;同 id 已存在则复用
transition.selectAll(selector) 子选择:所有匹配的后代 同上
transition.selectChild(selector) 子选择:第一个匹配的直接子元素 v7 新增
transition.selectChildren(selector) 子选择:所有匹配的直接子元素 v7 新增
transition.filter(filter) 过滤:仅保留匹配元素 选择器字符串或 (d, i, nodes) => bool
transition.merge(other) 与同 id 过渡合并,补全 null 位置 other 必须同 id
transition.transition() 在本过渡结束时接力新过渡 继承 name/duration/ease;delay 相对上段结束时刻
transition.selection() 返回过渡对应的 selection 过渡 → 选择世界的桥
active(node, name) 查询节点上活跃过渡 用于 on("start") 中重建循环动画

掌握以上 API 后,你就能覆盖 d3 动画实践中最常用的三种结构:多选择集同步动画(模板过渡 + 继承 timing)、增量局部动画(幂等子选择 + filter)、多阶段编排(transition.transition() 相对接力 + d3.active() 循环自举),并能在 docs/d3-transition/timing.mddocs/d3-transition/control-flow.md 的基础上进一步定制时序与控制流。

登录后查看全文
热门项目推荐
相关项目推荐