前言
d3中通过selection来操作元素,但selection并不能简单地被认为是“一个装了元素的数组”。本文学习自d3作者Mike Bostock的文章How Selections Work,同时添加了我的代码实践,代码均会放在CodePen上给大家自行调试
希望能起到理解selection的作用,不足或错误之处还请指出
selection是数组的子类
那什么是selection呢?
selection并不是一个DOM数组,它是数组的子类
与数组不同点有如下:
- 提供了操作被选中DOM元素的方法
- selection数组的元素是group
接下来的两个小标题将解释这两点
提供操作被选中DOM元素的方法
selection提供了操作被选中DOM元素的方法,比如selection.attr、selection.style等等,同时也继承了数组的一些方法,比如array.forEach、array.map。当然,我们一般不会使用这些原生方法,而是使用d3提供的更加方便的方法,比如selection.each
元素为group的数组
selection是一个元素为group的数组,每一个group是一个DOM数组
用图表可以这样表示,椭圆表示数组元素,中括号表示数组:
只有
selection.selectAll能够获得有多个group的selection。接下来我们以图表 + 代码 + 控制台打印来说明group,再说明group有什么作用
一个group的selection
<h1></h1>
<h1></h1>
<h1></h1>
<h1></h1>
<script>
const selection = d3.selectAll('h1');
<script>
对应控制台我们可以看见只有一个group
如果是
d3.select('h1')
则会选择一个h1标签
CodePen打开
多个group的selection
上文我们提到过只有selection.selectAll能够获得有多个group的selection
<h1>
<p></p>
<p></p>
</h1>
<h1>
<p></p>
<p></p>
</h1>
<script>
const h1Selection = d3.selectAll('h1');
const groupSelection = h1Selection.selectAll('p');
<script>
对应控制台,我们可以看见有多个group
每一个旧元素(此例中为h1)会变成一个group,group中包含旧元素下匹配的元素(此例中为p);同时每一个group会有对应的
父元素,此例中则为h1另外,d3.select与d3.selectAll的父元素均为html根元素
CodePen打开
group有什么作用
- 操作元素时,将相同层次结构的DOM元素以组划分,方便多组DOM元素的操作
- 数据绑定时,以group划分,方便多层次结构的数据绑定
这里我们先举操作元素的例子,数据绑定的例子在后文
group将相同层次结构的DOM元素以组划分,在进行多组DOM元素操作时更加方便,因此,selction.attr与selection.style所调用函数的第二个参数是在group范围的下标,而不是selection范围的下标
groupSelection.attr('some-attr', (data, index)=>{
console.log('元素在group范围内的下标:' + index);
})
CodePen打开
至此,我们可以看出selection并不能简单地认为是一个DOM元素数组,而是数组的子类,数组元素为group,同时提供了操作被选中DOM元素的方法
接下来我们进一步探索selection的相关操作
不会生成group的操作
只有selection.selectAll方法会生成含多个group的selection
因为select方法只会选中每个group一个元素,所以select方法会保存已有的group,同时传递已绑定的数据
const exampleData = [1, 2];
const h1Selection = d3.selectAll('h1');
const groupSelection = h1Selection.selectAll('p');
console.log(groupSelection);
groupSelection.data(exampleData)
const spanSelection = groupSelection.select('span');
console.log(spanSelection);
console.log(spanSelection.data());
CodePen打开
通过控制台打印,我们可以看见select方法
保存了原先存在的group,同时传递了数据同时,由于selection.append和selection.insert方法是在select之上进行封装的,所以他们也有保存group和传递数据的特性,这就非常方便了我们使子元素根据父元素的数据进行相对应的构建,例如:
我们把数据绑定在已有的div上,在div中插入p,就可以直接使用对应的数据进行操作了
数据绑定与selection
那么数据绑定是“绑”到哪里的呢?d3把数据绑定到DOM元素的__data__属性上而非selection的属性上。
const data = [10,11,12];
const selection = d3.selectAll('div').data(data);
console.log(selection);
const DOMList = document.querySelectorAll('div');
console.log(DOMList);
CodePen打开
我们可以看到DOM元素中即有对应绑定的数据
如果被selection选中的DOM元素没有数据,则会返回undefined
<div></div>
<div></div>
<div></div>
<div></div>
......
const data = [10,11,12];
const selection = d3.selectAll('div').data(data);
......
console.log(d3.selectAll('div').data());
CodePen打开
因此数据是一直绑定在DOM元素上的,selection变量是可以销毁的,我们完全可以重新选择DOM元素,重新获取先前绑定的数据并进行操作
同时我们列举下绑定数据的三种方法:
- selection.data
- sekectuib.datum
- 通过append、insert、select从父节点继承
selection.data与group
selection.data是以group为单位进行数据绑定的,而不是以每一个元素为单位
我们来看看多个group的情况
以上图为例,当有多个group时,selection.data(argument1)传入的argument1为函数相比数组变量更为合适,同时这个函数的参数也会有对应的group下标
我们还是以代码为例更容易理解
<div>
<p></p>
<p></p>
<p></p>
<p></p>
</div>
<!-- ...... -->
<!-- ......一共有三个类似的div组 -->
const dataArray = ['a', 'b', 'c', 'd'];
const groupSelection = d3.selectAll('div').selectAll('p');
console.log(groupSelection);
groupSelection.data((parentData, groupIndex)=>{
//console.log(parentData); //因为父元素无data绑定,所以返回undefined
console.log('groupIndex:' + groupIndex);
return dataArray;
});
console.log(groupSelection.data());
CodePen打开
此处groupSelection.data()传入的为函数,此函数的第一个参数表示parentData,第二个参数就表示上文提到的groupIndex,因为有三个group,此例中会执行三次数据绑定,我们也可以在控制台上看见按组划分的groupIndex
每次函数
return dataArray则对应绑定的数据此例仅为每个group都返回同样的一维数据,旨在说明的是groupIndex
这里让我们回想上文提到的group的另一个作用:数据绑定时,以group划分,方便多层次结构的数据绑定
当我遇上多层次数据对应多组group的时候,我们就可以减轻层级DOM及层级data的绑定
这里举一个简单的应用:
const hierarchicalData = [
['a', 'b', 'c', 'd'],
['e', 'f', 'g', 'h'],
['i', 'j', 'k', 'l']
];
.....
groupSelection.data((parentData, groupIndex)=>{
return hierarchicalData[groupIndex];
});
这样,就可以把对应的多层次数据分别绑定到group之中
当当selection只有一个group时我们传值就不必传函数了,直接传数组即可,一般只有在多个group的情况下才需要向selection.data()传函数
数据绑定匹配元素
当我们把数据绑定到元素上时,是通过pairing keys(不知道咋翻译)匹配的,key是一个不可重复的字符串,用于为数据绑定进行匹配
最简单的key是取元素对应下标,这里我直接截取Mike Bostock文中内容
第0个元素匹配data[0],第1个元素匹配data[1]。。。
如果数据和元素的顺序没变的话,index为key是很方便的,当顺序改变时,index就无法匹配到原先的元素或是数据了,这时我们需要第二个参数key function来指定key
我们再举一个例子,假设某组元素绑定了一组数据,元素顺序改变,我们希望
保持原有元素并绑定数据,而不是销毁重建我们继续以代码说明
const oldData = [
{ name: "A", value: "I am A" },
{ name: "B", value: "I am B" },
{ name: "C", value: "I am C" },
{ name: "D", value: "I am D" },
{ name: "E", value: "I am E" },
];
const selection = d3.selectAll('div').data(oldData);
......
//模拟元素重新排序,此行代码后元素会倒序
const sortedSelection = selection.sort((a, b)=>{
return -1;
});
//新数据
const newData = [
{ name: "B", value: "I am B!!!!!" },
{ name: "A", value: "I am A!!!!!" },
{ name: "C", value: "I am C!!!!!" },
{ name: "D", value: "I am D!!!!!" },
{ name: "E", value: "I am E!!!!!" },
];
//匹配绑定
sortedSelection.data(newData, (d)=>{
return d.name;
});
CodePen打开
我们在绑定数据后对DOM元素进行重排,再次绑定新的数据时根据d.name匹配,可以发现元素顺序仍为重排后的顺序,而并未重新创建
结语
在学习How Selections Work之后,我对selection及group有了更深的理解,group、数据传递及key parser在数据绑定、多层级数据上着实提供了极大的便利,此文可以说是个人的学习总结,如有不足或错误之处还请大佬指出