首页
/ d3-selection 控制流详解:each、call、nodes 等 7 个方法的用法与实现原理

d3-selection 控制流详解:each、call、nodes 等 7 个方法的用法与实现原理

2026-09-06 20:21:03作者:俞予舒Fleming

在 d3 的数据驱动绘图流程中,选中 DOM 元素只是第一步,真正的定制往往发生在“对选区做点什么”的阶段。d3-selection 为此提供了一组控制流 API:selection.eachselection.call 用于对每个选中元素执行任意代码或复用组件函数,nodesnodesizeempty[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(交给第三方库、挂载到页面、做测量计算),用 nodesnodesizeempty

本仓库的入口 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.mdd3.select(...).call(d3.brush().on("brush", brushed))docs/d3-drag.mdd3.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 与迭代器

eachcall 外,文档还定义了 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.mdd3.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] 展开

两条容易踩坑的语义:

  1. each / 迭代器只覆盖非空元素,空选区上不会触发回调,无需额外判空;
  2. call 的返回值不是被调函数的返回值。若组件函数(如 d3 的行为工厂)内部返回自身以支持其自身的链式写法,call 依然只把选区交还给你——这是设计约定,不是 bug。

验证与进一步阅读

本仓库对 d3-selection API 的汇出有自动化保证:test/d3-test.js 遍历 package.json 中的全部依赖(含 "d3-selection": "^3.0.0"),断言 d3 命名空间导出了子模块中的每一个属性(version 除外),即 eachcallnodes 等 7 个控制流方法都必然出现在 d3 聚合对象上。

各方法的底层实现位于 d3-selection 包的 src/selection/ 目录下,分别对应 each.jscall.jsempty.jsnodes.jsiterator.jsnode.jssize.js 七个源文件(该包以依赖形式引入,见 package.jsonsrc/index.js)。

延伸阅读,均位于本仓库文档中:

  • 选取元素select / selectAll 的查询语义,是控制流方法作用的对象来源;
  • 修改元素each 常与其组合使用;
  • 联结数据:enter/exit 之后,控制流方法决定了如何驱动后续处理;
  • 局部变量:利用 eachthis 上下文在元素上存取状态;
  • transition 控制流:过渡选区上形态对称的 each / nodes / node / empty
登录后查看全文
热门项目推荐
相关项目推荐