前言
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的經驗與心得,歡迎關注我的掘金賬號~