d3-selection 控制流详解:each、call、nodes 等 7 个方法的用法与实现原理
【免费下载链接】d3Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:项目地址: https://gitcode.com/GitHub_Trending/d3/d3
在 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。
【免费下载链接】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),仅供参考