
在 D3.js v7+ 中,join 的 enter 回调函数内无法直接按元素获取对应 datum(.datum() 仅返回首个数据项),需改用 selection.each() 遍历每个 DOM 节点并结合当前数据 d 实例化逻辑。
在 d3.js v7+ 中,`join` 的 enter 回调函数内无法直接按元素获取对应 datum(`.datum()` 仅返回首个数据项),需改用 `selection.each()` 遍历每个 dom 节点并结合当前数据 `d` 实例化逻辑。
D3.js 的 .join() 方法是声明式数据绑定的核心,但其 enter/update/exit 回调函数的设计初衷是批量操作,而非逐元素上下文感知。正如示例所示:当在 enter 回调中调用 enter.datum(),它返回的是整个 enter selection 的第一个绑定数据(即 data[0]),而非当前正在处理的元素所对应的 datum——这导致无法为每个 <li> 正确初始化不同 n 值的 Rects 实例。
✅ 正确做法是:先执行 .join() 获取完整 selection(含 enter、update、exit),再对 enter selection(或通用 selection)调用 .each((d, i, nodes) => { ... })。在 each 回调中,参数 d 即为当前 DOM 元素(nodes[i])所绑定的数据对象,可安全用于构造器参数、条件判断或动态配置。
以下是重构后的关键代码段(已优化可读性与健壮性):
使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
const data = [{n: 3, text: 'some text'}, {n: 2, text: 'some other text'}];
const listItems = d3.select('#root')
.selectAll('li')
.data(data)
.join('li'); // 返回包含 enter/update 的 selection
// ✅ 在 each 中访问每个元素的完整 datum
listItems.each(function(d, i) {
const li = d3.select(this); // 当前 <li> 元素
const rects = new Rects(d.n); // 使用 d.n 实例化 —— 数据驱动!
li.text(`There should be ${d.n} rects:`)
.append('svg')
.attr('width', d.n * 20 + 20)
.attr('height', 20)
.append('g')
.attr('transform', 'translate(20,0)')
.selectAll('rect')
.data([...Array(d.n).keys()])
.join('rect')
.attr('x', (i) => i * 20)
.attr('y', 0)
.attr('width', 20)
.attr('height', 20)
.attr('fill', (_, i) => ['purple', 'forestgreen', 'orange'][i % 3]);
});⚠️ 注意事项:
- 不要依赖
enter.datum()获取单个元素数据——它是 selection 级方法,非元素级; -
each()中的this指向原生 DOM 元素,务必用d3.select(this)包装以使用 D3 方法; - 若需区分 enter/update 逻辑(如仅对新元素初始化),可用
li.filter((d, i, nodes) => nodes[i].__data__ === d)或配合selection.enter()提前捕获,但多数场景each已足够; -
Rects.draw()原实现中硬编码了this.#domain,实际可简化为直接传入d.n,避免类耦合,提升复用性。
该模式体现了 D3 “数据驱动文档” 的本质:数据决定行为,每个元素在其上下文中独立响应自身数据。掌握 selection.each() 是突破 join 抽象层、实现精细化控制的关键一步。

















