一、文档资源
D3JS 中文文档:https://d3js.org.cn/document/
D3JS英文文档:https://github.com/d3/d3-selection/blob/main/README.md
D3选集(Selection)解析-附API文档翻译:https://juejin.cn/post/7004753817652854814
二、源码阅读
以下列代码为例,跟踪代码的执行。
var selection = d3.select("svg");
var data = [50,100];
var a = selection.selectAll("rect");
var b = a.data(data);
var c = b.join('rect');
var d=c.attr('x',d=>d).attr('y',d=>d);
var e = d.attr("width",50).attr("height", 50);
var onClick = (e,d)=>console.log("clicked");
e.on("click",onClick);
该段时序生成了两个正方形,连长为50和100。生成结果如下,点击矩形在控制台打印"clicked"消息:
1、 var selection=d3.select("svg")
// select.js
import {Selection, root} from "./selection/index.js";
export default function(selector) {
return typeof selector === "string"
? new Selection([[document.querySelector(selector)]], [document.documentElement])
: new Selection([[selector]], root);
}
如果是string,直接将selector传给dom方法 querySelector,根节点是document.documentElement ;所以querySelector支持的selector都可以使用。主要的selector有:
选择器 | 用法 |
---|---|
id选择器 | #myid |
类选择器 | .myclassname |
标签选择器 | div,h1,p |
相邻选择器 | h1+p |
子选择器 | ul > li |
后代选择器 | li a |
通配符选择器 | * |
属性选择器 | a[rel=“external”] |
伪类选择器 | a:hover, li:nth-child |
如果参数不是string(比如 null 或者 function),直接传递selector,root( [null] ) 给Selection对象的构造函数。新生成的Selection对象在导入的时候就已定义,定义为:
// selection/index.js
Selection.prototype = selection.prototype = {
constructor: Selection,
select: selection_select,
selectAll: selection_selectAll,
selectChild: selection_selectChild,
selectChildren: selection_selectChildren,
filter: selection_filter,
data: selection_data,
enter: selection_enter,
exit: selection_exit,
join: selection_join,
merge: selection_merge,
selection: selection_selection,
order: selection_order,
sort: selection_sort,
call: selection_call,
nodes: selection_nodes,
node: selection_node,
size: selection_size,
empty: selection_empty,
each: selection_each,
attr: selection_attr,
style: selection_style,
property: selection_property,
classed: selection_classed,
text: selection_text,
html: selection_html,
raise: selection_raise,
lower: selection_lower,
append: selection_append,
insert: selection_insert,
remove: selection_remove,
clone: selection_clone,
datum: selection_datum,
on: selection_on,
dispatch: selection_dispatch,
[Symbol.iterator]: selection_iterator
};
Selection类的构造函数对参数的处理仅是将参数传递给了内部变量。
// selection/index.js
export function Selection(groups, parents) {
this._groups = groups;
this._parents = parents;
}
2、 var a = selection.selectAll("rect");
如果有rect,返回rect,如果无,返回new Selection的空集。
// selection/selectAll.js
export default function(select) {
if (typeof select === "function") select = arrayAll(select);
else select = selectorAll(select);
for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
if (node = group[i]) {
subgroups.push(select.call(node, node.__data__, i, group));
parents.push(node);
}
}
}
return new Selection(subgroups, parents);
}
selectAll首先判断select参数是否为function ,如果是,调用arrayAll方法:
// selection/selectAll.js
// select 参数为selection.selectAll中传入的function
function arrayAll(select) {
return function() {
return array(select.apply(this, arguments));
};
}
这里提升了selectAll的功能,既能直接处理字符,也能处于函数,使用function的代码示例如下:
const sibling = d3.selectAll("p").selectAll(function() { return [ this.previousElementSibling, this.nextElementSibling ]; });
这段代码可以自定义返回所选节点的兄弟节点。
如果不是,调用selectAll.js中的selectAll方法。
回到本例,select为’rect’,因此直接转向selectAll方法,该方法定义如下:
// selectorAll.js
export default function(selector) {
return selector == null ? empty : function() {
return this.querySelectorAll(selector);
};
}
selector参数被转到dom的querySelectorAll方法。此时svg元素还没有rect元素。
然后进入两个for循环,生成新的元素集并指定父元素。用这两个参数生成新的selection。
for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) { for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { if (node = group[i]) { subgroups.push(select.call(node, node.__data__, i, group)); parents.push(node);
所以,每一次对selection使用selectAll方法,都会返回新的selection,selection===a的结果是false。
3、 var b = a.data(data);
这里完成了元素与data的绑定,返回新的selection,并附加了两个方法:enter和exit。
要实现绑定,内部需要处理很多工作,数据可能经现有元素多,也可以一样,也可能少。如果源数据比现有的元素多,则通过enter()返回多的这部元素集;如果少,现在的元素就得删除一些,通过exit()可以返回要删除的元素集。
3.1 主方法data方法
// selection/data.js
export default function(value, key) {
//如果data()参数为空,则返回__data__数据(如有)
if (!arguments.length) return Array.from(this, datum);
//bind为指定按序号还是按指定的key访问(如传入的是{[k:v]}类型)
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;//如文档中没有选择的元素,则为空集。
//如果value不是function,则调用constant函数(用于封装传入的值为function)。即将value范式化为function,function用于返回传入的数据。
if (typeof value !== "function") value = constant(value);
//update, enter, exit用于存储更新、新进入、删除的元素。
// 连续的selectAll方法,会使groups长度大于1。
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,
//返回数组形式,如[50,100]
data = arraylike(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方法为bindKey或者bindIndex。
// bindIndex按数据data和原有元素把不同节点放到对应容器(enter/update/exit)
// enter存储了新的节点,定义为EnterNode,其中数据会被存到__data__
// 由于enterGroup /updataGroup数组长度是一样的,对于非新入,非update的位置值为空。
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/enter/exit各自存各自的数据,而且返回了一个新的Selection对象,
//含有的是update的数据
//绑定操作需要进一步进行(join)。
update = new Selection(update, parents);
//链接至enter和exit方法,但还未执行。
update._enter = enter;
update._exit = exit;
return update;
}
上述代码中的datum函数
function datum(node) { return node.__data__; }
上述代码中的constant函数
// constant.js export default function(x) { return function() { return x; }; }
上述代码中的arraylike函数
// data.js function arraylike(data) { return typeof data === "object" && "length" in data ? data // Array, TypedArray, NodeList, array-like : Array.from(data); // Map, Set, iterable, string, or anything else }
上述代码中的EnterNode
// enter.js export function EnterNode(parent, datum) { this.ownerDocument = parent.ownerDocument; this.namespaceURI = parent.namespaceURI; this._next = null; this._parent = parent; this.__data__ = datum; }
3.2 bindIndex方法
// data.js
function bindIndex(parent, group, enter, update, exit, data) {
//groups为空,
var i = 0,
node,
groupLength = group.length,
dataLength = data.length;
// Put any non-null nodes that fit into update.
// Put any null nodes into enter.
// Put any remaining data into enter.
// 根据要绑定的数据长度来看,由于是绑定index,所以如果原来有元素,就先更新;
// 更新完如果data还没结束,就应该是enter。
for (; i < dataLength; ++i) {
if (node = group[i]) {
node.__data__ = data[i];
update[i] = node;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// Put any non-null nodes that don’t fit into exit.
// 此时i并未初始化,沿用上面的,指向了data之后。
// 所以所以如果data结束了但原来的元素还有,就迭代并加入exit。
for (; i < groupLength; ++i) {
if (node = group[i]) {
exit[i] = node;
}
}
}
3.3 bindKey方法
// selection/data.js
function bindKey(parent, group, enter, update, exit, data, key) {
var i,
node,
nodeByKeyValue = new Map,
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 = key.call(node, node.__data__, i, group) + "";
if (nodeByKeyValue.has(keyValue)) {
exit[i] = node;
} else {
nodeByKeyValue.set(keyValue, node);
}
}
}
4、 var c = b.join('rect');
join方法接受onXXX系列3个参数(onenter, onupdate, onexit),参数可以是3个function(onenter还可以是string)。function可以被自动传入相应的Selection数据。最后返回的是合并的selection(如果有新增加的数据)或者更新后的selection。
- 4.1 主代码
// selection/join.js
export default function(onenter, onupdate, onexit) {
//执行enter()、exit()方法。获得对应的选择集。
var enter = this.enter(), update = this, exit = this.exit();
if (typeof onenter === "function") {
enter = onenter(enter);
if (enter) enter = enter.selection();
} else {
//enter是选择集,append是selection.append方法,最后会使用dom的appendChild方法添加元素
enter = enter.append(onenter + "");
}
if (onupdate != null) {
update = onupdate(update);
if (update) update = update.selection();
}
if (onexit == null) exit.remove(); else onexit(exit);
return enter && update ? enter.merge(update).order() : update;
}
上述代码中的enter和exit方法。
// selection/enter.js export default function() { return new Selection(this._enter || this._groups.map(sparse), this._parents); }
// selection/exit.js export default function() { return new Selection(this._exit || this._groups.map(sparse), this._parents); }
- 4.2 append方法
- 用于根据选择集向dom树添加元素,使用dom的appendChild方法。
// selection/append.js
export default function(name) {
var create = typeof name === "function" ? name : creator(name);
return this.select(function() {
return this.appendChild(create.apply(this, arguments));
});
}
上述代码中的creator方法,进行命名空间处理,最后调用dom的createElement或者createElementNS生成新节点
// creator.js export default function(name) { var fullname = namespace(name); return (fullname.local ? creatorFixed : creatorInherit)(fullname); }
creator代码中的namespace方法
// namespaces.js export default function(name) { var prefix = name += "", i = prefix.indexOf(":"); if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1); return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins }
creator代码中的creatorInherit方法
//creator.js function creatorInherit(name) { return function() { var document = this.ownerDocument, uri = this.namespaceURI; return uri === xhtml && document.documentElement.namespaceURI === xhtml ? document.createElement(name) : document.createElementNS(uri, name); }; }
4.2 merge方法
合并enter和update选择集。因enter和update中只在对应位置存储新进入和更新的数据,而其他位置是空,用一个新的merges数据,所以通过遍历enter和update,可以合并非空值,实现数据和元素的同步。最后返回的是一个新Selection对象。
//selection/merge.js
export default function(context) {
var selection = context.selection ? context.selection() : context;
for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
if (node = group0[i] || group1[i]) {
merge[i] = node;
}
}
}
for (; j < m0; ++j) {
merges[j] = groups0[j];
}
return new Selection(merges, this._parents);
}
5、var d=c.attr('x',d=>d).attr('y',d=>d);
6、 var e = d.attr("width",50).attr("height", 50);
这里会使用selection.each方法,迭代元素并调用回调函数。
回调函数处理了传入attr的参数,如果attr只有一个参数,则为返回属性值;如果是null,则删除属性;如果是function,执行function,根据返回结果删除属性(null)或者调用dom的setAttribute方法设置属性;如果是数值,将会被传递给setAttribute。
返回值是this,并未生成新选择集。
// selection/attr.js
export default function(name, value) {
var fullname = namespace(name);
if (arguments.length < 2) {
var node = this.node();
return fullname.local
? node.getAttributeNS(fullname.space, fullname.local)
: node.getAttribute(fullname);
}
return this.each(
(value == null?
//如果为null,删除属性
(fullname.local ? attrRemoveNS : attrRemove)
:
(
//如果是function,调用attrFunction/attrFunctionNS,否则使用attrConstant/attrConstantNS
typeof value === "function"?
(fullname.local ? attrFunctionNS : attrFunction)
:
(fullname.local ? attrConstantNS : attrConstant)
)
)(fullname, value));
}
上述代码中的each函数
// selection/each.js export default function(callback) { for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) { for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) { if (node = group[i]) callback.call(node, node.__data__, i, group); } } return this; }
7、 e.on("click",onClick);
on接受三个参数typename, value, options。typename是事件名,value是回调函数,options可选,是addEventListener的附加参数。
on函数最终会调用dom的addEventListener。如果参数小于2,由取消事件监听。使用each迭代,通过onAdd处理添加逻辑。
// selection/on.js
export default function(typename, value, options) {
// 利用parseTypenames实现参数标准化
var typenames = parseTypenames(typename + ""), i, n = typenames.length, t;
if (arguments.length < 2) {
var on = this.node().__on;
if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
for (i = 0, o = on[j]; i < n; ++i) {
if ((t = typenames[i]).type === o.type && t.name === o.name) {
return o.value;
}
}
}
return;
}
on = value ? onAdd : onRemove;
for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));
return this;
}
上述代码中的parseTypenames和onAdd函数
// selection/on.js function parseTypenames(typenames) { return typenames.trim().split(/^|\s+/).map(function(t) { var name = "", i = t.indexOf("."); if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i); return {type: t, name: name}; }); }
function onAdd(typename, value, options) { return function() { var on = this.__on, o, listener = contextListener(value); if (on) for (var j = 0, m = on.length; j < m; ++j) { if ((o = on[j]).type === typename.type && o.name === typename.name) { this.removeEventListener(o.type, o.listener, o.options); this.addEventListener(o.type, o.listener = listener, o.options = options); o.value = value; return; } } this.addEventListener(typename.type, listener, options); o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options}; if (!on) this.__on = [o]; else on.push(o); }; }
三、总结
d3.selection实现了dom元素与数据的对应,将对数据的迭代转化为对dom元素的迭代操作,且能自动向回调函数传送数据,简化了复杂的for循环逻辑。