d3-selection 控制流详解:each、call、nodes 等 7 个方法的用法与实现原理
在 d3 的数据驱动绘图流程中,选中 DOM 元素只是第一步,真正的定制往往发生在“对选区做点什么”的阶段。d3-selection 为此提供了一组控制流 API:selection.each、selection.call 用于对每个选中元素执行任意代码或复用组件函数,nodes、node、size、empty 与 [Symbol.iterator] 则用于把选区还原为普通 DOM 节点数组以配合原生 JavaScript。读完本文,你将能熟练掌握这 7 个方法的签名、参数与返回值差异,并结合本仓库中的真实示例理解它们在坐标轴、缩放、拖拽等组件中被复用的方式。
控制流方法在 d3-selection 中的定位
selection 模块总览 把 d3-selection 的能力划分为六大主题:选取元素(selecting)、修改元素(modifying)、数据联结(joining)、事件处理(events)、控制流(control flow)与局部变量(locals)。其中控制流(Control flow)文档的定位是:“For advanced usage, selections provide methods for custom control flow”——面向进阶用法,让选区能够以开发者自定义的方式遍历与调用。
也就是说,控制流方法是 d3 选区体系里的“逃生舱口”:
- 当你需要逐元素执行任意逻辑(例如同时访问父子数据、操作
this上下文),用each或迭代器; - 当你需要把选区当作参数传给可复用函数并保持链式调用,用
call; - 当你需要跳出 d3 API、直接操作原生 DOM(交给第三方库、挂载到页面、做测量计算),用
nodes、node、size、empty。
本仓库的入口 src/index.js 通过 export * from "d3-selection" 把这些 API 原样汇入 d3 命名空间,因此下面所有方法都可以通过 d3.select(...) / d3.selectAll(...) 返回的选区对象直接调用。
selection.each(function):逐元素执行回调
签名:selection.each(function)
行为:按 DOM 顺序对每个选中元素调用一次指定函数。函数收到的参数为:
| 参数 | 含义 |
|---|---|
d(第一个参数) |
当前元素绑定的数据(current datum) |
i(第二个参数) |
当前元素在选区中的索引 |
nodes(第三个参数) |
当前选区组(group),即整个节点数组 |
this |
当前 DOM 元素本身(即 nodes[i]) |
典型用途是创建能同时访问父数据与子数据的作用域。原文档给出的示例:
parent.each(function(p, j) {
d3.select(this)
.selectAll(".child")
.text(d => `child ${d.name} of ${p.name}`);
});
这里外层 each 回调中的 p 是父元素的数据,this 是父 DOM 元素;通过 d3.select(this) 从父元素内部再选出所有 .child,于是内部回调里的 d(子数据)与 p(父数据)同时可见。这种“父上下文 + 子选区”的组合是 each 最经典的实战场景,例如渲染一组尺寸各异的子环图(donut multiples)。
仓库中 locals.md 的多个示例也依赖 each 提供的 this 上下文来在元素上读写局部变量,modifying.md 则展示 each 结合 selection.classed 等修改方法的用法。可以推断:凡是回调里需要“以当前元素为出发点再选一遍”的场景,each 都是首选入口。
与迭代器的区别:each 的回调参数是 (d, i, nodes),而原生 for...of 迭代只返回节点本身,需要额外通过 d3.select(node).datum() 取数据。两者可互为补充。
selection.call(function, ...arguments):可复用组件的关键
签名:selection.call(function, ...arguments)
行为:恰好一次调用指定函数,把当前选区作为第一个参数、其余参数按原样传入;无论被调函数返回什么,call 本身始终返回该选区。这与手工调用函数等价,但让链式调用得以延续。
文档示例——把“设置若干样式”封装为可复用函数:
function name(selection, first, last) {
selection
.attr("first-name", first)
.attr("last-name", last);
}
然后:
d3.selectAll("div").call(name, "John", "Snow");
大致等价于:
name(d3.selectAll("div"), "John", "Snow");
唯一的区别是:selection.call 永远返回 selection 而不是被调函数 name 的返回值。这个“返回值恒为选区”的语义正是它能无缝嵌入 append → call → attr → transition 链式调用的原因。
仓库中的真实用例
call 是 d3 组件化设计的基石,本仓库文档与组件代码中大量出现:
- 坐标轴:docs/d3-axis.md 中所有示例都以
svg.append("g").call(d3.axisBottom(x))挂载坐标轴; - 刷选与拖拽:docs/d3-brush.md 用
d3.select(...).call(d3.brush().on("brush", brushed)),docs/d3-drag.md 用d3.selectAll(".node").call(d3.drag().on("start", started)),模式完全一致——行为工厂(behavior factory)+call; - 示例组件:ExampleBlankChart.vue 中,x 轴与 y 轴分别通过
.call(d3.axisBottom(x))和.call(d3.axisLeft(y))附加到svg上:
// Add the x-axis.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x));
// Add the y-axis.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y));
- 与 transition 联动:ExampleAxis.vue 展示了
call的另一常见姿势——作用于过渡选区:d3.select(g.value).transition().duration(props.duration).call(props.axis)。这说明call的契约(接收选区、不关心其“类型”)同样适用于 transition 选区,而 d3-transition 的控制流文档 也为过渡选区提供了同名的each/nodes/node/empty方法,两边 API 形态对称。
选区自省:nodes、node、size、empty 与迭代器
除 each 与 call 外,文档还定义了 5 个用于检查与解包选区的方法。注意这 5 个方法都只统计/返回非空(non-null)元素——选区中的 null 节点(例如 selectAll 时匹配不到的空位)会被统一忽略。
selection.nodes()
签名:selection.nodes()
返回本选区中所有非空元素构成的数组:
d3.selectAll("p").nodes() // [p, p, p, …]
等价于:
Array.from(selection)
适合把选区交给不认 d3 选区对象的第三方代码,例如 console.table、测量函数或 DOM 操作库。
selection.node()
签名:selection.node()
返回选区中第一个非空元素;若选区为空则返回 null。
仓库中有两个典型用法:
- ExampleArcs.vue 构建完扇形 SVG 后
return svg.node();,把 d3 选区还原为单个可挂载的 DOM 节点; - ExampleBlankChart.vue 在图表组装完毕后用
this.$el.append(svg.node())将整棵 SVG 挂载进 Vue 组件容器; - docs/d3-zoom.md 中
d3.zoomTransform(selection.node())——把当前缩放变换查询建立在第一个节点之上。
selection.size()
签名:selection.size()
返回选区中非空元素的总数,是“这个选区有多少东西”的快速答案。
selection.empty()
签名:selection.empty()
当选区不含任何非空元素时返回 true:
d3.selectAll("p").empty() // false, here
常用作防御性判断:
if (selection.empty()) { /* 选区为空,走回退逻辑 */ }
selectionSymbol.iterator
签名:selection[Symbol.iterator]()
返回一个遍历选区中(非空)元素的迭代器。这使选区成为可迭代对象(iterable),两种典型用法:
// 迭代选区中的每个元素
for (const element of selection) {
console.log(element);
}
// 展开为普通数组
const elements = [...selection];
由于实现了标准的迭代协议,选区还可以配合 Array.from、解构、includes 检查等现代 JavaScript 语法直接使用。
七个方法的速查与返回语义对比
| 方法 | 返回值 | 典型场景 |
|---|---|---|
each(fn) |
原选区 | 逐元素执行任意代码,回调内可用 this(当前节点)与 (d, i, nodes) 三元组 |
call(fn, ...args) |
原选区(恒为选区,与被调函数返回值无关) | 复用组件函数(坐标轴、缩放、拖拽等),保持链式调用 |
nodes() |
非空元素数组 | 交给原生 DOM API / 第三方库 |
node() |
首个非空元素,空选区为 null |
挂载单节点、zoomTransform 等单节点查询 |
size() |
非空元素数量 | 数量判断、断言 |
empty() |
布尔值 | 空选区防御 |
[Symbol.iterator]() |
迭代器 | for...of、[...selection] 展开 |
两条容易踩坑的语义:
each/ 迭代器只覆盖非空元素,空选区上不会触发回调,无需额外判空;call的返回值不是被调函数的返回值。若组件函数(如 d3 的行为工厂)内部返回自身以支持其自身的链式写法,call依然只把选区交还给你——这是设计约定,不是 bug。
验证与进一步阅读
本仓库对 d3-selection API 的汇出有自动化保证:test/d3-test.js 遍历 package.json 中的全部依赖(含 "d3-selection": "^3.0.0"),断言 d3 命名空间导出了子模块中的每一个属性(version 除外),即 each、call、nodes 等 7 个控制流方法都必然出现在 d3 聚合对象上。
各方法的底层实现位于 d3-selection 包的 src/selection/ 目录下,分别对应 each.js、call.js、empty.js、nodes.js、iterator.js、node.js、size.js 七个源文件(该包以依赖形式引入,见 package.json 与 src/index.js)。
延伸阅读,均位于本仓库文档中:
- 选取元素:
select/selectAll的查询语义,是控制流方法作用的对象来源; - 修改元素:
each常与其组合使用; - 联结数据:enter/exit 之后,控制流方法决定了如何驱动后续处理;
- 局部变量:利用
each的this上下文在元素上存取状态; - transition 控制流:过渡选区上形态对称的
each/nodes/node/empty。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00