前言
D3.js作为著名的数据可视化框架,在自定义图表领域是无可争议的No.1。使用频率最高的api当属d3.select
,因此它被称为"svg界的jquery"(目前已经支持canvas)。jquery中有this
,那么D3.js中当然也有this
。比如如下代码:
d3.selectAll("p").on("click", function() {
d3.select(this).style("color", "red");
});
上述代码是一个简单的事件绑定和响应。其中的this
指向哪里呢?
(以下分析与结论均基于v4版本。)
javascript中的this
这真是一个老掉牙的话题了,随便百度谷歌一下应该就会有无数篇文章了。简单来说this
指向调用它的对象,仅此而已。其他的本文不再也没必要赘述啦。
D3.js中的this
常规事件中this的指向及实现
继续完善上述示例代码,并打印以下this
:
<body>
<p>one</p>
<p>two</p>
<p>three</p>
<p>four</p>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
d3.selectAll("p").on("click", function() {
console.log(this);
d3.select(this).style("color", "red");
});
</script>
</body>

点击以后我们看到this
指向的就是DOM,与document.getElementById()
这样的方法返回的是同样的结果。那么D3是如何让this指向DOM的呢?
这就要求助于源码了。D3.js的源码阅读起来非常舒服,不像React那样找一个函数要跳很大几段或者横跨多个文件,反而更像诗一样一行一行写成,不过也与其本身的简洁的设计思想有关。我们看下selection/on.js
的源码:
function(typename, value, capture) {
var typenames = parseTypenames(typename + ""), i, n = typenames.length, t;
on = value ? onAdd : onRemove;
if (capture == null) capture = false;
for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
return this;
}
typenames
是一个将输入的事件类型字符串进行格式化的函数,我们暂时不用管它。与addEventListener
类似,value
参数即为传入的listener function
。通过三元表达式的判断,on
将被赋值onAdd
,我们看下onAdd
的实现:
function onAdd(typename, value, capture) {
var wrap = filterEvents.hasOwnProperty(typename.type) ? filterContextListener : contextListener;
return function(d, i, group) {
var on = this.__on, o, listener = wrap(value, i, group);
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.capture);
this.addEventListener(o.type, o.listener = listener, o.capture = capture);
o.value = value;
return;
}
}
this.addEventListener(typename.type, listener, capture);
o = {type: typename.type, name: typename.name, value: value, listener: listener, capture: capture};
if (!on) this.__on = [o];
else on.push(o);
};
}
onAdd
返回一个函数,首先会将type
,name
,value
等参数作为对象存在变量o
中,如果一个DOM元素绑定了多个事件,那么将这些数据集o
依次存入数组内。接着对数组on
进行遍历,依次调用addEventListener
方法。
分析到这里我们知道了,selection.on(typenames[, listener[, capture]])
方法实际上就是调用原生的addEventListener
,而根据MDN文档的内容,listener
中的this
默认指向绑定事件的元素。所以对于上述的示例代码,我们可以简写成这样:
addEventListener('click',function(){
// ...
console.log(this)
})
综上可以得出这样的结论:D3.js事件监听函数中的this
与原生事件相同,指向绑定对应事件的DOM元素。
D3.js的拖拽事件与this
既然事件都是用类似addEventListener
来实现的,那D3.js中常用的drag
事件是不是也是addEventListener(drag,fn)
的形式去实现呢?阅读下v4文档答案是否定的:
d3.selectAll(".node").call(d3.drag().on("start", started));
很明显比原生的写法麻烦了许多,而且居然有call
方法,我们知道call
是用来改变this
的指向,但传入call
的参数似乎又跟this
没什么关系,为什么要这样写呢?
最开始这个问题我也思索了很久,从未见过call
方法这么用的场景。直到我打开源码,发现原来作者很调皮的把call
方法重写了,此call
非彼call
,它的作用更像是唤起(如果作者把这个方法命名为invoke
我就不用走弯路了)。那么看下call.js
的实现:
function() {
var callback = arguments[0];
arguments[0] = this;
callback.apply(null, arguments);
return this;
}
很简单,把上述代码的d3.drag().on("start", started)
赋值给callback
,再把此时的this
,也就是d3.selectAll('node')
中每一个node
赋值给arguments[0]
,然后使用apply
方法将arguments
作为参数传入callback
中。这样做的好处是什么呢?
举个例子,我们想基于D3.js设计一个设置class属性的函数,可能会这么写:
function setClass(selection,class1,class2){
selection.attr('class1',class1);
selection.attr('class2',class2);
};
setClass(d3.selectAll("div"), "header", "footer");
现在有了重写的call
方法,我们就可以使用更快捷的链式调用写法:
d3.selectAll('div').call(setClass,'header','footer');
依据上面对call
函数的分析我们可以观察到,setClass
赋值给了callback
,d3.selectAll('div')
赋值给了arguments[0]
,接着将d3.selectAll('div')
,header
,footer
作为参数传入setClass
,这样就实现了第一段代码直接调用setClass
函数的逻辑。可以说,call
方法是作者利用this
特性而设计的语法糖。
总结
上述内容主要记述和讲解了关于D3.js中this
的主要使用场景。毕竟是发布于2011年的框架,那时候这样数据驱动的框架还是非常新颖的,但和近几年的MVVM等思潮相比,D3.js的学习和开发成本确实高了不少。在掘金上D3.js相关资料少得可怜,近期我会多分享几篇对于D3.js的经验与心得,欢迎关注我的掘金账号~