前言
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 的经验与心得, 欢迎关注我的掘金账号~
来源: https://juejin.im/post/5a7411845188254e76177d3c