前言
最近在学习d3.js,刚开始到选择器这一节,有一个selection.data方法,发现这个方法第二个参数可传可不传,第二个参数是函数,但文档里面并没有第二个参数使用方式的讲解,很难找到列子,所以就干脆撸源码了。
这里values是数组,key为一个函数;大多数例子里面写的都是不传第二个函数参数,今天就传第二个参数的情况讲解。
先看一段代码
body里面有这些div,每个上面有一个class。
const data = [
{ key: "Locke", number: 4 },
{ key: "Reyes", number: 8 },
{ key: "Ford", number: 15 },
{ key: "Jarrah", number: 16 },
{ key: "Shephard", number: 23 },
{ key: "Kwon", number: 42 }
];
d3.selectAll("div")
.data(data, function (d) {
return d ? d.key : this.className;
}).text(d=>d.key)
这样就把数据根据class名字给填充了
Ford
Jarrah
Kwon
Locke
Reyes
Shephard
然后就疑问了?why?为什么这么写,函数里面谁教你的这么写?你怎么知道要这么写?很遗憾,我没找到说明文档
那就看源码了,上一段代码
Selection.prototype = selection.prototype = {
constructor: Selection,
select: selection_select,
selectAll: selection_selectAll,
filter: selection_filter,
data: selection_data,
...
这一段我们发现Selection.prototype里面有个data方法,对应的是selection_data,那我们就去找selection_data,
function selection_data(value, key) {
if (!value) {
data = new Array(this.size()), j = -1;
this.each(function(d) { data[++j] = d; });
return data;
}
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;
if (typeof value !== "function") value = constant$1(value);
for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = value.call(parent, parent && parent.__data__, j, parents),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// Now connect the enter nodes to their following update node, such that
// appendChild can insert the materialized enter node before this node,
// rather than at the end of the parent node.
for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) i1 = i0 + 1;
while (!(next = updateGroup[i1]) && ++i1 < dataLength);
previous._next = next || null;
}
}
}
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
}
这个函数接受两个参数value, key;当key存在的时候var bind = key ? bindKey : bindIndex,这时候我们先知道这个bind赋值就是bindkey,就是绑定key,然后下面大概意思是
一个for循环,然后处理变量,然后生成了一个update,返回undate;
这个过程有几个参数传给了bind函数,
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = value.call(parent, parent && parent.__data__, j, parents),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
看过d3开头文档的人都应该是到enter,exit,group,update,就是选择器生成的对象;
enter意思就是数据个数比dom个数多出来的部分,将要插入的节点;
exit意思就是dom个数比数据个数比多出来的部分,将要删除的部分
update意思是将要更新的部分。
不明白的先看一下文档,再反过来看这个。
那么bind函数就是bindkey,看一下bindkey是啥
function bindKey(parent, group, enter, update, exit, data, key) {
var i,
node,
nodeByKeyValue = {},
groupLength = group.length,
dataLength = data.length,
keyValues = new Array(groupLength),
keyValue;
// Compute the key for each node.
// If multiple nodes have the same key, the duplicates are added to exit.
for (i = 0; i < groupLength; ++i) {
if (node = group[i]) {
keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
if (keyValue in nodeByKeyValue) {
exit[i] = node;
} else {
nodeByKeyValue[keyValue] = node;
}
}
}
// Compute the key for each datum.
// If there a node associated with this key, join and add it to update.
// If there is not (or the key is a duplicate), add it to enter.
for (i = 0; i < dataLength; ++i) {
keyValue = keyPrefix + key.call(parent, data[i], i, data);
if (node = nodeByKeyValue[keyValue]) {
update[i] = node;
node.__data__ = data[i];
nodeByKeyValue[keyValue] = null;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// Add any remaining nodes that were not bound to data to exit.
for (i = 0; i < groupLength; ++i) {
if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
exit[i] = node;
}
}
}
可以看到它接收到这些个变量,然后三个循环,这三个循环都是围绕nodeByKeyValue,exit,那么先看第一个循环,
这路 groupLength 就是获取的dom节点的长度,group是一个dom数组;
nodeByKeyValue开始是一个空对象{}
exit先不用管他,意思就是生成将要移除的部分dom节点数组
keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
这句的意思
keyPrefix 内部一个常量吧 打印出来是 '$'
key是你传的函数参数
node = group[i]就是当前遍历的dom节点
function (d) {
return d ? d.key : this.className;
}
函数内部this指向node,函数可带三个参数node.__data__, i, group,
node.__data__ 是dom节点上的__data__,
i 索引
group是dom数组
当我们执行
d3.selectAll("div")
.data(data, function (d) {
return d ? d.key : this.className;
}).text(d=>d.key)
的时候第一个遍历我们可以得到的结果是nodeByKeyValue
{
$Ford: div.Ford
$Jarrah: div.Jarrah
$Kwon: div.Kwon
$Locke: div.Locke
$Reyes: div.Reyes
$Shephard: div.Shephard
}
函数内的this.className是节点的className,这个时候每个节点上面还没有__data__ 属性,所以你打印d是undefined;
这时候我们的函数已经执行了5次,因为有5个dom节点。
记住nodeByKeyValue,后面有用
那么我们再看第二个循环,很明显遍历的是data,
for (i = 0; i < dataLength; ++i) {
keyValue = keyPrefix + key.call(parent, data[i], i, data);
if (node = nodeByKeyValue[keyValue]) {
update[i] = node;
node.__data__ = data[i];
nodeByKeyValue[keyValue] = null;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
与刚才的逻辑差不多,还是要执行我们的函数
这次第一个参数是data[i],先生成
keyValue = keyPrefix + key.call(parent, data[0], i, data);//$Locke
node = nodeByKeyValue[keyValue] // node=nodeByKeyValue.$Locke
node.__data__ = data[i]; // node.__data__ = { key: "Locke", number: 4 }
到这里已经看到数据已经跟dom对应了。
到目前为止函数执行了groupLength + dataLength次;
至于其他代码相关代码,大家可以去看看,这次就不看了。
我的感觉:D3的源码写法并不高级,里面也会有冗余的代码。