D3 v7 过渡(Transition)高级控制流详解:生命周期、interrupt、end 与事件监听
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
本文以 d3-transition 控制流文档 为主体,系统讲解 D3 过渡从创建、调度、启动、插值到结束的完整生命周期,以及selection.interrupt、interrupt、transition.end()、transition.on等高级控制流 API 的语义、边界与报错时机,并辅以transition.each、transition.call等工具方法与仓库依赖结构的源码级佐证。读完后,你将能够准确控制多个过渡之间的打断与同步、通过 Promise 等待过渡完成、注册start/end/interrupt/cancel四类过渡事件,并理解 D3 为何要按帧批量执行 tween 以提升性能。
一、先理解前提:过渡是选择(selection)的“动画版”
在 D3 中,过渡是一种与 selection 类似、但用于把 DOM 从当前状态平滑插值到目标状态的接口,典型入口是selection.transition()(详见 元素选择文档 与 d3-transition 总览):
d3.select("body") .transition() .style("background-color", "red");过渡创建后,可以用 delay、duration、attr、style 等方法进行配置。本文聚焦的是“高级用法”——控制流(control flow):过渡何时开始、如何被打断、如何等待结束、事件何时触发。这也是官方文档 docs/d3-transition/control-flow.md 开篇的定位:“For advanced usage, transitions provide methods for custom control flow.”
二、过渡的一生(The Life of a Transition)
这是控制流文档的核心章节,理解它才能正确理解后面所有 API 的可用窗口。整个生命周期可以划分为五个阶段,每个阶段对“还能改什么”有严格限制:
2.1 创建后:配置窗口(同步求值与延迟求值)
创建过渡之后(例如通过selection.transition()或transition.transition()),可以立即用delay、duration、attr、style等方法配置。这里有一个关键的求值时机区别:
- 指定目标值的方法(如
transition.attr)同步求值——调用时立即评估; - 需要起始值参与插值的方法(如
transition.attrTween、transition.styleTween)必须推迟到过渡启动时才能求值,因为起始值(当前 DOM 状态)要等到过渡开始才能确定。
2.2 调度(scheduled):本帧末尾或下一帧
过渡创建后不久——当前帧结束时或下一帧期间——会被调度(scheduled)。从这一刻起:
delay与start事件监听器不能再修改;- 尝试修改会抛出错误,消息为"too late: already scheduled";
- 若过渡已经结束,则抛出"transition not found"。
2.3 启动(start):打断与取消发生在这里
过渡随后启动时,发生三件重要的事:
- 打断(interrupt):若同一元素上存在同名的活动过渡,它会被打断,并向已注册的监听器派发
interrupt事件。 - 取消(cancel):启动过渡会取消同一元素上在它之前创建的同名待处理(pending)过渡。
- 派发
start事件:向监听器派发。
两个容易踩坑的细节(文档原文明确强调):
- 打断发生在“启动”时,而不是“创建”时。因此即使是一个零延迟(zero-delay)的过渡,也不会立刻打断活动中的旧过渡——旧过渡会被保留“最后一帧”。如果需要立即打断,必须显式调用
selection.interrupt()(见下节)。 start事件派发后是最后一次可以修改过渡的时机:运行中的过渡的 timing、tween 和监听器都不能再改,强行修改会抛出"too late: already running"(若已结束则为 "transition not found")。- 过渡在启动后立即初始化其 tweens。
2.4 运行中:按帧调用 tween,且批量初始化
- 过渡启动的那一帧,但在所有启动于本帧的过渡都启动之后,过渡第一次调用它的 tweens。
- 这种批量(batching)初始化 tween的做法通常涉及读取 DOM,D3 将其集中处理,目的是避免 DOM 读与写交叉进行(interleaved DOM reads and writes),从而提升性能。
- 在过渡活跃的每一帧,它以经过 缓动(eased) 的
t值(0 到 1 之间)调用 tweens;在单帧内部,tweens按注册顺序依次调用。
2.5 结束(end):最终帧与配置销毁
- 过渡结束时,用**未经缓动(non-eased)**的
t = 1再调用一次 tweens,保证终点值精确落地; - 随后派发
end事件。这是最后一次可以检查(inspect)过渡的时机:结束之后,过渡从元素上删除,其配置被销毁(在打断或取消时配置同样会被销毁); - 在销毁之后再尝试检查该过渡,会抛出"transition not found"。
把三个报错信息串起来,就是一张“操作窗口”检查表:
| 阶段 | 还允许做什么 | 越界操作的结果 |
|---|---|---|
| 创建后、调度前 | 配置 delay/duration/tween/listeners | — |
| 已调度、未启动 | 不能再改 delay 与start监听器 | 抛 "too late: already scheduled" |
| 运行中 | 不能再改 timing/tweens/listeners | 抛 "too late: already running" |
| 已销毁 | 不能再检查过渡 | 抛 "transition not found" |
三、selection.interrupt(name):显式打断活动过渡
selection.interrupt(name)打断所选元素上指定name的活动过渡,并取消指定name的待处理过渡(如果有);若不指定name,则使用null。
文档特别指出一个非递归语义:打断某个元素上的过渡,不会影响任何子孙元素上的过渡。这一点在组合组件中至关重要——例如 轴过渡(axis transition) 并不是单个过渡,而是由轴<g>元素子孙元素上的多个相互独立但同步的过渡组成(刻度线、刻度标签、域路径等)。因此要打断一个轴过渡,必须打断其子孙:
selection.selectAll("*").interrupt();其中通用选择器*会选中所有子孙元素。如果还想连<g>元素本身一起打断:
selection.interrupt().selectAll("*").interrupt();四、interrupt(node, name):面向单节点的底层版本
interrupt(node, name)是面向单个 DOM 节点的等价操作:打断指定节点上指定name的活动过渡,并取消该name的待处理过渡(如果有);不指定name时使用null。它与selection.interrupt互为姊妹 API——前者作用于选择集合,后者直接作用于单个节点,适合在非 selection 上下文中(例如原生 DOM 事件处理器里拿到this或event.currentTarget后)使用。
五、transition.end():用 Promise 等待过渡完成
transition.end()返回一个Promise:当所有被选中的元素都完成过渡时 resolve;如果任一元素的过渡被取消(cancel)或打断(interrupt),该 Promise 会被 reject。
这为“过渡完成后做后续动作”提供了基于 Promise 的现代写法(例如在await之后再执行 DOM 移除、或触发下一个流程分支),而不再必须依赖end事件回调。注意它与第二节的呼应:由于 interrupt/cancel 会使 Promise reject,用它串接业务流程时要显式catch或判断场景。
六、transition.on(typenames, listener):四类过渡事件
transition.on(typenames, listener)为每个被选中元素添加或移除指定事件typenames的listener。支持的事件类型共有四种:
start—— 过渡启动时;end—— 过渡结束时;interrupt—— 过渡被打断时;cancel—— 过渡被取消时。
需要特别强调(文档原文明确指出):这些不是原生 DOM 事件(与selection.on/selection.dispatch实现的机制不同),而是transition 事件,由过渡机制本身派发。
以下细节直接决定监听器的行为是否符合预期:
- 命名空间:类型后可以可选地跟一个点号(
.)和一个名字,从而允许同一类型注册多个回调,例如start.foo与start.bar; - 多个 typenames:用空格分隔,如
interrupt end或start.foo start.bar; - 回调参数:当过渡事件在某个选中节点上派发时,listener 以该过渡元素为上下文调用,接收当前数据
d、当前索引i、当前分组nodes,this为当前 DOM 元素; - 数据与索引的语义:监听器总能拿到元素的最新 datum,但索引是选择(selection)的属性,在监听器被分配时就固定了——要更新索引需重新分配监听器;
- 替换与移除:同一元素上若已为相同typename注册过监听器,旧监听器会被移除后再加新的。移除单个监听器传
null作为 listener;移除某名字下的所有监听器,传null且typename写.foo(foo为该名字);移除所有未命名监听器,typename写.; - getter 形式:不传 listener 时,返回第一个(非 null)选中元素上该typename当前已分配的监听器(如有);指定多个 typenames 时返回第一个匹配的监听器。
七、过渡上的控制流工具方法:each / call / empty / nodes / node / size
文档后半部分收录了过渡对象上的一组“直通选择”的控制流方法。它们与 d3-selection 同名方法 语义等价,让你无需先transition.selection()拿到选择即可操作。
transition.each(function)
为每个选中元素调用指定函数,传入当前数据d、索引i、分组nodes,this为当前 DOM 元素。可用于为每个元素执行任意代码,特别适合构建同时访问父级与子级数据的上下文。等价于selection.each。
transition.call(function, ...arguments)
调用指定函数恰好一次,传入本过渡以及任意可选参数,并返回本过渡以便链式调用——即“手动调用函数的链式包装”。文档给出的可复用配色示例:
function color(transition, fill, stroke) { transition .style("fill", fill) .style("stroke", stroke); }于是可以这样使用:
d3.selectAll("div").transition().call(color, "red", "blue");它等价于手动展开:
color(d3.selectAll("div").transition(), "red", "blue");这种写法让“给一个过渡批量设置若干属性/样式”的公共逻辑可以封装成独立函数并复用,等价于selection.call。
empty / nodes / node / size 速查
| 方法 | 返回 | 等价于 |
|---|---|---|
transition.empty() | 过渡是否不含任何(非 null)元素(布尔) | selection.empty |
transition.nodes() | 过渡中所有(非 null)元素组成的数组 | selection.nodes |
transition.node() | 第一个(非 null)元素;过渡为空时返回null | selection.node |
transition.size() | 过渡中元素总数 | selection.size |
这四个方法与生命周期检查直接相关:例如在start监听器里用transition.node()拿到元素、用transition.nodes()做一次性批量处理,都属于“start 之后仍是合法检查窗口”内的安全操作;而一旦过渡结束,过渡本身已被销毁,只能操作节点而不能再操作过渡。
八、结合本仓库:依赖结构、版本与文档质量如何验证
本仓库是 D3 的顶层发行包,当前版本为7.9.0(见 package.json)。控制流文档所描述的d3-transitionAPI 在本仓库中的落地方式可以从三处确认:
- 依赖声明:package.json 的
dependencies中声明"d3-transition": "^3.0.1",说明本文所述行为对应 d3-transition 3.x 系列 API;yarn.lock 中将其解析为 d3-transition 3.0.1 的发布产物。 - 统一再导出:src/index.js 以
export * from "d3-transition";(第 29 行)将该模块的全部公共 API——包括interrupt、active、transition构造器及transition.on、transition.end等——并入 D3 顶层命名空间。因此d3.active(node, name)、d3.interrupt(node, name)等函数在import * as d3 from "d3"后均可直接调用。 - 文档锚点校验:test/docs-test.js 会递归爬取
docs/下全部 Markdown,收集标题与{#anchor}形式的显式锚点,并校验所有内部链接指向的锚点真实存在(documentation links point to existing internal anchors)。这意味着 docs/d3-transition/control-flow.md 中如{#selection_interrupt}、{#transition_end}等锚点以及指向 timing、selecting、modifying 的链接,在仓库测试层面是被持续保障的。
从源码结构看,本仓库并不内嵌 d3-transition 的实现源码——它作为独立 npm 包通过依赖引入,文档中的 “Source” 指向该独立仓库的src/transition/*.js(如end.js、on.js)与src/selection/interrupt.js、src/interrupt.js。因此若需逐行研读控制流实现(例如 tween 批量调度的具体帧循环、too late错误的抛出位置),应以 d3-transition 包为准;本仓库提供的是 API 契约、版本约束与文档锚点保障。
九、适用前提与限制小结
- 版本前提:本文内容以本仓库 package.json 声明的
d3-transition ^3.0.1与 D3 7.9.0 为准;行为描述继承自 docs/d3-transition/control-flow.md,适用于 d3-transition 3.x 文档所对应的过渡机制。 - 事件语义限制:
start/end/interrupt/cancel是过渡事件而非原生 DOM 事件,不能通过selection.on或selection.dispatch那套 DOM 事件机制触发或监听。 - 修改窗口限制:三个错误消息("too late: already scheduled"、"too late: already running"、"transition not found")划定了 delay、监听器、timing、tween 各自的可修改边界,任何自动化脚本都应按此窗口安排调用时机。
- 打断粒度限制:
interrupt不递归到子孙元素;打断组合组件(如轴)的过渡时,必须显式selectAll("*").interrupt()覆盖其子孙。 end()的 reject 语义:transition.end()在任一元素被取消或打断时 reject,跨过渡编排时必须处理该分支。
掌握上述生命周期时间线与各 API 的边界,就能在 D3 中可靠地实现过渡的打断、同步、链式编排与 Promise 化收尾——这正是 d3-transition 控制流文档 所定义的“高级用法”的完整闭环。
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考