剖析 D3.js 中的 this 相關

前言

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

javascript中的this

這真是一個老掉牙的話題了,隨便百度谷歌一下應該就會有無數篇文章了。簡單來講this指向調用它的對象,僅此而已。其餘的本文再也不也不必贅述啦。node

D3.js中的this

常規事件中this的指向及實現

繼續完善上述示例代碼,並打印如下thisjquery

<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元素。框架

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賦值給了callbackd3.selectAll('div')賦值給了arguments[0],接着將d3.selectAll('div')headerfooter做爲參數傳入setClass,這樣就實現了第一段代碼直接調用setClass函數的邏輯。能夠說,call方法是做者利用this特性而設計的語法糖。

總結

上述內容主要記述和講解了關於D3.js中this的主要使用場景。畢竟是發佈於2011年的框架,那時候這樣數據驅動的框架仍是很是新穎的,但和近幾年的MVVM等思潮相比,D3.js的學習和開發成本確實高了很多。在掘金上D3.js相關資料少得可憐,近期我會多分享幾篇對於D3.js的經驗與心得,歡迎關注個人掘金帳號~

相關文章
相關標籤/搜索