D3.js做爲著名的數據可視化框架,在自定義圖表領域是無可爭議的No.1。使用頻率最高的api當屬d3.select
,所以它被稱爲"svg界的jquery"(目前已經支持canvas)。jquery中有this
,那麼D3.js中固然也有this
。好比以下代碼:javascript
d3.selectAll("p").on("click", function() {
d3.select(this).style("color", "red");
});
複製代碼
上述代碼是一個簡單的事件綁定和響應。其中的this
指向哪裏呢?
(如下分析與結論均基於v4版本。)java
這真是一個老掉牙的話題了,隨便百度谷歌一下應該就會有無數篇文章了。簡單來講this
指向調用它的對象,僅此而已。其餘的本文再也不也不必贅述啦。node
繼續完善上述示例代碼,並打印如下this
:jquery
<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的呢?canvas
這就要求助於源碼了。D3.js的源碼閱讀起來很是舒服,不像React那樣找一個函數要跳很大幾段或者橫跨多個文件,反而更像詩同樣一行一行寫成,不過也與其自己的簡潔的設計思想有關。咱們看下selection/on.js
的源碼:api
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
方法。
bash
分析到這裏咱們知道了,selection.on(typenames[, listener[, capture]])
方法實際上就是調用原生的addEventListener
,而根據MDN文檔的內容,listener
中的this
默認指向綁定事件的元素。因此對於上述的示例代碼,咱們能夠簡寫成這樣:app
addEventListener('click',function(){
// ...
console.log(this)
})
複製代碼
綜上能夠得出這樣的結論:D3.js事件監聽函數中的this
與原生事件相同,指向綁定對應事件的DOM元素。框架
既然事件都是用相似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的經驗與心得,歡迎關注個人掘金帳號~