D3 源代碼解析(二)

這是繼上一篇D3源碼解構文章後的對D3的研究筆記,筆者的能力有限,若有哪裏理解錯誤,歡迎指正。javascript

對集合的操做

關於d3.attr

一個能夠處理不少狀況的函數,當只傳入一個參數時,若是是string,則返回該屬性值,若是是對象,則遍歷設置對象的鍵值對屬性值,若是參數大於等於2,則是普通的設置樣式:css

var node = d3.select('body')

node.attr('class')
> 返回該屬性值

node.attr('class', 'haha')
> 設置該屬性值

node.attr({'class': 'haha', 'x': '10'})
> 設置該屬性值

那麼怎麼作到一個函數處理多種狀況,很明顯是根據參數的數量來區別對待:html

d3_selectionPrototype.attr = function(name, value) {
    if (arguments.length < 2) {
      if (typeof name === "string") {
        var node = this.node();
        name = d3.ns.qualify(name);
        return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name);
      }
      for (value in name) this.each(d3_selection_attr(value, name[value]));
      return this;
    }
    return this.each(d3_selection_attr(name, value));
  };

關於getAttributeNS咱們能夠不用理會,對於web端,d3在設置和獲取屬性的時候用的都是getAttribute和setAttribute。
對於d3_selection_attr函數,它返回一個通用函數,該函數會對當前對象設置對應的屬性值:
大概的思想:java

function d3_selection_attr(name, value) {
  return function() {
    this.setAttribute(name, value);
  }
}

selection.classed

具體用法能夠看文檔介紹,大概的意思是若是有鍵值對或者對象傳入,則根據value值來添加或刪除name類,不然則檢測是否含有該類, 若是selection有多個,只檢測第一個並返回該值node

var line = d3.selectAll('line');
line.classed('a b c d', true)
>對全部節點設置class
line classed({'a': true, 'b': false})
>分別添加和刪除類

和attr同樣,經過對參數長度和類型的區分,執行不一樣的方法jquery

d3_selectionPrototype.classed = function(name, value) {
    if (arguments.length < 2) {
      if (typeof name === "string") {
        var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1;
        if (value = node.classList) {
          while (++i < n) if (!value.contains(name[i])) return false;
        } else {
          value = node.getAttribute("class");
          while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
        }
        return true;
      }
      for (value in name) this.each(d3_selection_classed(value, name[value]));
      return this;
    }
    return this.each(d3_selection_classed(name, value));
  };

這裏考慮到傳入的字符串可能含有多個類名,d3_selection_classes函數用來分割:css3

return (name + '').trim().split(/^|\s+/)

這裏涉及到一個小細節,先用trim過濾掉字符串兩邊的空白字符,而後用正則表達式去分割類名,正則表達式中的s匹配任何空白字符,包括空格、製表符、換頁符等等。等價於 [ fnrtv],並且還有一個^,它在這裏應該是匹配第一個的意思,測試了一下,發現若是不加這個匹配的話,對於空白字符串不會返回長度爲0的數組,而是會返回含有一個空字符串長度爲一的數組,因此這應該是爲了防止出現這種狀況而作的匹配,不過原理仍是不懂。對於正則的組合,暫時不理解加^就能防止該問題的緣由。git

關於匹配是否存在該類,爲了防止匹配的時候發生類名爲’asdf',測試的類名爲'a',因爲包含關係而被匹配成功,因此不能簡單的使用indexOf的方法,而是要使用正則表達式去作匹配,因爲類名要麼在最開始,要麼在中間兩邊有空格,要麼在末尾,因此使用github

new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g")

去作正則匹配web

這裏用到了(?:pattern)的方法,意思是匹配 pattern 但不獲取匹配結果,也就是說這是一個非獲取匹配,不進行存儲供之後使用。這在使用 "或" 字符 (|) 來組合一個模式的各個部分是頗有用。例如, 'industr(?:y|ies) 就是一個比 'industry|industries' 更簡略的表達式。

d3_selectionPrototype.style

和attr結構相似的函數,特別在於若是傳入的值是函數,則會分別對每一個元素調用一次函數,並傳入元素和元素的位置、優先級等

d3_selectionPrototype.style = function(name, value, priority) {
    var n = arguments.length;
    if (n < 3) {
      if (typeof name !== "string") {
        if (n < 2) value = "";
        for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
        return this;
      }
      if (n < 2) {
        var node = this.node();
        return d3_window(node).getComputedStyle(node, null).getPropertyValue(name);
      }
      priority = "";
    }
    return this.each(d3_selection_style(name, value, priority));
  };

關於樣式的設置,d3用的是style.getProperty(name)和style.setProperty(name, x, priority)
樣式的獲取,用的是和jquery的實現方法,具體能夠看看鑫大大的文章

通常咱們用的是window.getComputedStyle(elem, '僞類')還有IE自娛自樂的currentStyle, 具體的細節就不說了。
二者的不一樣在於getPropertyValue只能獲取設置在style中的屬性,而window.getComputedStyle則會獲得元素最終顯示在頁面上的綜合樣式,就算沒有顯示聲明也能夠拿到,這點是最重要的區別。

selectionPrototype.propertyselectionPrototype.text

property 給元素設置額外的屬性,例如:
node.property('bar', 'hahahaha')
node.property('bar') // hahahaha

text 設置元素的文本,是經過element.textContent來設置文本的,以前咱們設置文本和html都是經過innerText和innerHTML去設置,那麼這和textContent有什麼區別嗎?

實驗
筆者測試了下在Chrome和firefox下的狀況,發現最新版本的瀏覽器其實都是支持二者的,不過innerText並非w3c標準,因此之前firefox並不支持innerText。

二者的區別

  • 轉義上,textContent對傳入的文本若是帶有n等換行符,不會忽略,而innText會忽略並轉義爲空格

  • textContent會獲取全部子節點的文本,而innerText不會理會隱藏節點的文本。

selectionProperty.html

這個沒什麼好講的,封裝了innerHTML的方法

d3_selectionPrototype.append

比較特別的是實現的代碼:

d3_selectionPrototype.append = function(name) {
    name = d3_selection_creator(name);
    return this.select(function() {
      return this.appendChild(name.apply(this, arguments));
    });
  };

函數中返回一個函數的執行結果,該執行函數中又返回一個函數的執行結果,層層嵌套卻又很是聰明的作法,咱們從最裏面的一層看,首先對當前的節點添加子元素,而後返回該子節點元素,最後再經過select方法獲取該子元素。

d3_selectionPrototype_creator(name) {
  function create() {
    return document.createElement(name);
  }
  return typeof name == 'function' ? name : create;
}

這是簡易版本的creator,d3還要考慮到在xml中的狀況,xml建立子節點調用的是document.createElementNS,d3是經過namespaceURI來判斷頁面類型的吧,不過在MDN上查詢發現這個屬性已經被列爲廢詞,隨時可能被廢除的,查詢了版本4,發現仍是沿用了這個屬性,這個比較危險吧。

d3_selectionPrototype.insert && d3_selectionPrototype.remove

insertBefore
同append相似,不過是封裝了insertBefore的方法,注意須要用元素節點才能調用該方法,正確的調用方法是:
existNodeParents.insertBefore(newNode, existNodeToBeInsertBefore)
remove
很簡單的實現:

function d3_selectionRemove() {
    var parent = this.parentNode;
    if (parent) parent.removeChild(this);
  }

Data

關於d3_selectionPrototype.data函數

這個函數是D3常用到也是比較關鍵的函數,用它來進行數據的綁定、更新,具體解析能夠參考上一篇文章D3源代碼解構
這裏涉及到一個特殊的屬性data,若是不傳入參數,data會返回全部算中集合元素的屬性值(property),可是爲何是經過node.__data__拿到的,經過搜索,終於找到了綁定該值得函數(一開始還覺得是DOM的隱藏變量- -)

d3_selectionPrototype.datum = function(value) {
    return arguments.length ? this.property("__data__", value) : this.property("__data__");
  };

若是傳入參數,它會建立三個特殊的私有變量,分別是

  • enter = d3_selection_enter([])

  • update = d3_selection([])

  • exit = d3_selection([])
    咱們能夠知道update和exit都是一個繼承了d3_selectionPrototype原型對象的數組,因此它擁有咱們上面提到的selectionPrototype全部的方法,而enter比較特殊,它單獨使用一套原型方法,實現方法以下:

function d3_selection_enter(selection) {
    d3_subclass(selection, d3_selection_enterPrototype);
    return selection;
  }
  var d3_selection_enterPrototype = [];
  d3.selection.enter = d3_selection_enter;
  d3.selection.enter.prototype = d3_selection_enterPrototype;
  d3_selection_enterPrototype.append = d3_selectionPrototype.append;
  d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
  d3_selection_enterPrototype.node = d3_selectionPrototype.node;
  d3_selection_enterPrototype.call = d3_selectionPrototype.call;
  d3_selection_enterPrototype.size = d3_selectionPrototype.size;
  d3_selection_enterPrototype.select = function(selector) {
    var subgroups = [], subgroup, subnode, upgroup, group, node;
    for (var j = -1, m = this.length; ++j < m; ) {
      upgroup = (group = this[j]).update;
      subgroups.push(subgroup = []);
      subgroup.parentNode = group.parentNode;
      for (var i = -1, n = group.length; ++i < n; ) {
        if (node = group[i]) {
          subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
          subnode.__data__ = node.__data__;
        } else {
          subgroup.push(null);
        }
      }
    }
    return d3_selection(subgroups);
  };
  d3_selection_enterPrototype.insert = function(name, before) {
    if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);
    return d3_selectionPrototype.insert.call(this, name, before);
  };

而後調用bind函數對傳入的data和key(可選)進行數據綁定,咱們知道d3會根據傳入的數據和已有的元素進行一一對應,一開始覺得是基於什麼算法去對應,看代碼實現就發現若是咱們不傳入key參數,其實就是簡單的索引對應:

function bind(group, groupData) {
      var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData;
      if (key) {
        var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue;
        for (i = -1; ++i < n; ) {
          if (node = group[i]) {
            if (nodeByKeyValue.has(keyValue = key.call(node, node.__data__, i))) {
              exitNodes[i] = node;
            } else {
              nodeByKeyValue.set(keyValue, node);
            }
            keyValues[i] = keyValue;
          }
        }
        for (i = -1; ++i < m; ) {
          if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) {
            enterNodes[i] = d3_selection_dataNode(nodeData);
          } else if (node !== true) {
            updateNodes[i] = node;
            node.__data__ = nodeData;
          }
          nodeByKeyValue.set(keyValue, true);
        }
        for (i = -1; ++i < n; ) {
          if (i in keyValues && nodeByKeyValue.get(keyValues[i]) !== true) {
            exitNodes[i] = group[i];
          }
        }
      } else {
        for (i = -1; ++i < n0; ) {
          node = group[i];
          nodeData = groupData[i];
          if (node) {
            node.__data__ = nodeData;
            updateNodes[i] = node;
          } else {
            enterNodes[i] = d3_selection_dataNode(nodeData);
          }
        }
        for (;i < m; ++i) {
          enterNodes[i] = d3_selection_dataNode(groupData[i]);
        }
        for (;i < n; ++i) {
          exitNodes[i] = group[i];
        }
      }

而當咱們傳入了key後,這個時候就不同了,D3會根據咱們傳入的這個函數去將元素和數據作綁定和更新、退出,這個key函數會在三次循環中分別被調用,一次是檢查是否有已經綁定了數據的元素,並初始化一個映射集合,第二次進行數據綁定元素,肯定update和enter集合,第三次肯定exit集合。
建議先看看官方文檔,瞭解具體的用法在看代碼會清晰不少。通俗的說,假設咱們傳入的數據有主鍵即惟一區分每一個數據的屬性,那麼,咱們即可以告訴data說用這個屬性來區分,也就是:

selection.data(mydata, function(d, i) {
  return d.主鍵名稱
}

關於d3_map集合能夠參考d3_map解析

Animation & Interaction (動畫和交互)

[d3_selectionPrototype.datum]()

這是上面講到的一個函數datum,惋惜在data中其實沒有用到,我遍歷了整個代碼只有一處地方調用了這個函數,它和data相似用來獲取或者設置元素的值,它是基於property上進行一層封裝,可是和data不一樣的是它沒有所謂的enter、exit集合返回,那麼它有什麼用呢?咱們能夠看看這篇文章

d3_selectionPrototype.filter

能夠傳入函數或者選擇器字符串進行集合的過濾

d3的事件監聽機制

看d3關於事件監聽的實現,看到了關於JS事件的一個屬性relatedTarget,關於JS的event對象以前接觸的很少,忽然看到關於這個屬性,上網查找資料,才發現了這麼冷門的屬性:

relatedTarget 事件屬性返回與事件的目標節點相關的節點。
對於 mouseover 事件來講,該屬性是鼠標指針移到目標節點上時所離開的那個節點。
對於 mouseout 事件來講,該屬性是離開目標時,鼠標指針進入的節點。
對於其餘類型的事件來講,這個屬性沒有用。

怎麼樣,夠冷門吧,只對兩種事件生效

還有一個方法叫作compareDocumentPosition,比較兩個節點,並返回描述它們在文檔中位置的整數
1:沒有關係,兩個節點不屬於同一個文檔。
2:第一節點(P1)位於第二個節點後(P2)。
4:第一節點(P1)定位在第二節點(P2)前。
8:第一節點(P1)位於第二節點內(P2)。
16:第二節點(P2)位於第一節點內(P1)。
32:沒有關係,或是兩個節點是同一元素的兩個屬性。
註釋:返回值能夠是值的組合。例如,返回 20 意味着在 p2 在 p1 內部(16),而且 p1 在 p2 以前(4)。

知道了這兩個屬性,d3的一個函數就看懂了:

function d3_selection_onFilter(listener, argumentz) {
    var l = d3_selection_onListener(listener, argumentz);
    return function(e) {
      var target = this, related = e.relatedTarget;
      if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) {
        l.call(target, e);
      }
    };
  }

獲取事件對應的對象和相關的對象,若是不存在相關的對象或者相關的對象不等於當前對象且相關對象不在當前對象以內,則執行監聽函數。

function d3_selection_onListener(listener, argumentz) {
    return function(e) {
      var o = d3.event;
      d3.event = e;
      argumentz[0] = this.__data__;
      try {
        listener.apply(this, argumentz);
      } finally {
        d3.event = o;
      }
    };
  }

這個函數返回一個函數,返回的函數綁定了當前對象並執行。

var d3_selection_onFilters = d3.map({
    mouseenter: "mouseover",
    mouseleave: "mouseout"
  });
  if (d3_document) {  
    d3_selection_onFilters.forEach(function(k) {
      if ("on" + k in d3_document) d3_selection_onFilters.remove(k);
    });
  }

D3還作了一個事件 映射,將mouseenter映射爲mouseover,mouseleave映射爲mouseout,而後判斷環境中是否有這兩個事件,若是有的話就取消這個映射。

以上三段代碼都是爲了處理執行環境中沒有mouseenter和mousemove狀況下如何利用mouseover和mouseleave去實現相同效果的問題。而後經過下面這個函數來判斷:

function d3_selection_on(type, listener, capture) {
    var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener;
    if (i > 0) type = type.slice(0, i);
    var filter = d3_selection_onFilters.get(type);
    if (filter) type = filter, wrap = d3_selection_onFilter;
    function onRemove() {
      var l = this[name];
      if (l) {
        this.removeEventListener(type, l, l.$);
        delete this[name];
      }
    }
    function onAdd() {
      var l = wrap(listener, d3_array(arguments));
      onRemove.call(this);
      this.addEventListener(type, this[name] = l, l.$ = capture);
      l._ = listener;
    }
    function removeAll() {
      var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match;
      for (var name in this) {
        if (match = name.match(re)) {
          var l = this[name];
          this.removeEventListener(match[1], l, l.$);
          delete this[name];
        }
      }
    }
    console.log('d3_selection_on:', i, listener, i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll);
    return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll;
  }

如今再來看這個函數就能夠看懂了,首先它判斷傳入的事件類型是否含有'.',由於D3在實現事件綁定時,會清除同種事件類型以前綁定的監聽函數,因此對於同一類型的事件,若是要綁定多個監聽函數,那麼就須要使用click.foo*click.bar*這種方式去進行區分,防止舊的事件被覆蓋掉,查看onAdd函數就能夠知道每次添加事件監聽的時候,就會調用onRemove去清除該事件監聽。

關於capture,默認是false,表示在冒泡階段響應事件,若是設置爲true,則是在捕獲階段響應事件,能夠參考這篇文章,這是歷史遺留緣由,好像當初的瀏覽器響應事件的設置不是冒泡階段,而是捕獲階段,後來爲了兼容而給了這個參數。

好了,懂得了D3事件綁定的原理,那麼實現這個函數就很容易,同樣的根據參數的數量和類型作不一樣的處理就行了:

d3_selectionPrototype.on = function(type, listener, capture) {
    var n = arguments.length;
    if (n < 3) {
      if (typeof type !== "string") {
        if (n < 2) listener = false;
        for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
        return this;
      }
      if (n < 2) return (n = this.node()["__on" + type]) && n._;
      capture = false;
    }
    return this.each(d3_selection_on(type, listener, capture));
  };

[d3.mouse]()

MDN上關於svg的一些屬性
一篇關於svg的講解
關於svg座標轉換爲屏幕座標.aspx)
關於使用矩陣轉換的實現
咱們要知道一些新的屬性:

  • ownerSVGElement】,用來獲取這個元素最近的svg祖先,沒有的話就返回元素自己。

  • svg.createSVGPoint】這個函數不在MDN中,看下MF的介紹.aspx),大概意思是初始化一個不在document文檔內的座標點

  • getScreenCTM

當咱們獲取網頁上鼠標的座標點的時候,能夠很簡單地調用e.clientXY,或者e.pageXY,可是svg有本身的一套座標系,它能夠自身旋轉、平移,因此咱們想知道按鈕點擊的位置相對於svg元素的位置時,須要考慮這些因素,從而使得獲取鼠標在svg的位置時變得沒那麼容易,再加上各類瀏覽器的坑……
這個時候就是線性代數就用上了(感謝線代老師!),忘的差很少的能夠參考上面的幾篇文章,svg自身已經提供了對應的矩陣運算,節省了咱們的一些實現的代碼。
再看看D3的代碼,就知道原做者也是被坑過的:

function d3_mousePoint(container, e) {
    if (e.changedTouches) e = e.changedTouches[0];
    var svg = container.ownerSVGElement || container;
    if (svg.createSVGPoint) {
      var point = svg.createSVGPoint();
      if (d3_mouse_bug44083 < 0) {
        var window = d3_window(container);
        if (window.scrollX || window.scrollY) {
          svg = d3.select("body").append("svg").style({
            position: "absolute",
            top: 0,
            left: 0,
            margin: 0,
            padding: 0,
            border: "none"
          }, "important");
          var ctm = svg[0][0].getScreenCTM();
          d3_mouse_bug44083 = !(ctm.f || ctm.e);
          svg.remove();
        }
      }
      if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX, 
      point.y = e.clientY;
      point = point.matrixTransform(container.getScreenCTM().inverse());
      return [ point.x, point.y ];
    }
    var rect = container.getBoundingClientRect();
    return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ];
  }

clientX是獲取相對於瀏覽器屏幕的座標,減去元素相對於屏幕的左邊距,爲了兼容IE等坑爹的默認開始位置爲(2,2),減去container的clienLeft,最終獲得svg的鼠標位置,但真的是爲了獲取相對的位置麼,須要再看看。

Behavior

[d3的touch、drag、touches]()

看不太懂這幾個的實現,和本身沒有怎麼使用到這幾個函數有關吧

[d3.zoom]()

zoom函數的實現,大概知道它經過綁定mouseWheel事件去記錄了放縮的值、中心、放縮位置等。也是涉及到event的綁定,表示hin暈。

D3的顏色空間

具體能夠參考前一篇文章

d3.xhr

D3對於ajax的實現,沒有兼容IE6及6如下的xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
只考慮了window.XMLHttpRequest,由於老版本的IE壓根就沒法正常使用各類圖形和動畫。

D3的timer的實現有點厲害

當咱們要用D3實現一個永久循環的動畫的時候,就可使用timer函數,向這個函數傳入一個函數,timer函數會在每一個動畫針中調用傳入的函數直至該函數返回‘true’,因此只要咱們始終不返回true就行了。
若是是這麼簡單固然就好實現了,可是若是有多個timer怎麼去控制呢?這個問題致使了實現的方法複雜了不少,直接上代碼:

var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_frame = this[d3_vendorSymbol(this, "requestAnimationFrame")] || function(callback) {
    setTimeout(callback, 17);
  };
  d3.timer = function() {
    d3_timer.apply(this, arguments);
  };
  function d3_timer(callback, delay, then) {
    var n = arguments.length;
    if (n < 2) delay = 0;
    if (n < 3) then = Date.now();
    var time = then + delay, timer = {
      c: callback,
      t: time,
      n: null
    };
    if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer;
    d3_timer_queueTail = timer;
    if (!d3_timer_interval) {
      d3_timer_timeout = clearTimeout(d3_timer_timeout);
      d3_timer_interval = 1;
      d3_timer_frame(d3_timer_step);
    }
    return timer;
  }
  
  function d3_timer_step() {
    var now = d3_timer_mark(), delay = d3_timer_sweep() - now;
    if (delay > 24) {
      if (isFinite(delay)) {
        clearTimeout(d3_timer_timeout);
        d3_timer_timeout = setTimeout(d3_timer_step, delay);
      }
      d3_timer_interval = 0;
    } else {
      d3_timer_interval = 1;
      d3_timer_frame(d3_timer_step);
    }
  }
  // 當即執行時間隊列,而後清洗掉已經結束的事件。
  d3.timer.flush = function() {
    d3_timer_mark();
    d3_timer_sweep();
  };
  // 遍歷時間隊列,若是回調函數返回真,則將該事件的回調賦值爲空,而後繼續檢查下一個,最後返回當前時間。
  function d3_timer_mark() {
    var now = Date.now(), timer = d3_timer_queueHead;
    while (timer) {
      if (now >= timer.t && timer.c(now - timer.t)) timer.c = null;
      timer = timer.n;
    }
    return now;
  }
  // 時間事件隊列的清洗,循環遍歷隊列中的時間對象,若是回調函數爲空,去掉,不然檢測下一個,最後返回最近要執行的事件時間點。
  function d3_timer_sweep() {
    var t0, t1 = d3_timer_queueHead, time = Infinity;
    while (t1) {
      if (t1.c) {
        if (t1.t < time) time = t1.t;
        t1 = (t0 = t1).n;
      } else {
        t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n;
      }
    }
    d3_timer_queueTail = t0;
    return time;
  }

D3使用隊列的方法實現,每次有新的timer進來,判斷隊列是否爲空,若是爲空,就將Head和隊尾指向它,不然,將隊尾和隊尾的下一個指向它

if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer;
    d3_timer_queueTail = timer;

感謝C和C++,告訴我指針實現鏈表的概念!

而後開始執行回調函數。

if (!d3_timer_interval) {
      d3_timer_timeout = clearTimeout(d3_timer_timeout);
      d3_timer_interval = 1;
      d3_timer_frame(d3_timer_step);
    }

timer_frame的實現是兼容了老版本的瀏覽器沒有 requestAnimationFrame 而退而使用setTimeout去實現,若是不太清楚這個api的同窗能夠看看鑫旭的這篇文章或者上MDN查。
而後每一個幀都會調用d3_timer_step這個函數,它調用了d3_timer_mark和d3_timer_sweep函數,循環遍歷了一遍時間隊列,而後獲取最近的待執行的時間點,獲得了delay時間差,當時間差大於24而且不爲Infinity的時候,便從新設置時間器,讓其在delay ms後執行,減小性能的消耗,若爲Infinity,表示沒有時間事件等待調用,中止了遞歸,不然,delay小於24ms,遞歸調用d3_timer_frame。

那麼爲何爲24ms呢?咱們知道瀏覽器的最佳動畫幀是60fbps,算起來每一幀的間隔爲1000/60 = 16.7ms,因此若是使用setTimeout實現動畫針的話,d3選擇的時間間隔是17ms,由於過小的話會出現掉幀的狀況,那麼這個和24有什麼關係呢?爲何要設定爲24呢?我也不清楚...在github上面提交了issues,不知道會不會有人解答,好緊張。
關於timer的一些擴展:
timer實現永久動畫
做者的實現

早上提交的issue下午原做者就給了回覆,不過做者的解釋就尷尬了,大概的意思就是因爲setTimeout的不穩定和不許確,存在必定的延遲,因此在設定這個值的時候也是拍腦殼設置的,值恰好在16.7到33.4之間,並回復說左右偏移都不會有什麼影響就對了。

[d3關於number 的方法:formatPrefix 和 round]()

提供了將number轉化爲特定格式的字符串方法,基於正則表達作匹配,而後對應地作轉化。這部分的實現比較瑣碎,就沒去仔細研究了,有興趣的能夠看看。

[d3.time]()

一樣的,將d3.time初始化爲一個空對象,而且將window.Date對象設置爲私有變量:d3_date = Date
萬物皆爲我所用!
首先咱們要了解Date的UTC函數,UTC() 方法可根據世界時返回 1970 年 1 月 1 日 到指定日期的毫秒數。
而後來看這個函數:

function d3_date_utc() {
    this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]);
  }

這個函數是一個構造函數,當咱們new d3_date_utc(xxx)的時候,它會建立一個日期對象,並根據咱們傳入的參數數量去建立,若是咱們傳入的參數多餘1個,那麼很顯然咱們傳入的是年月日這些參數,那麼便調用 Date.UTC.apply去返回時間戳,若是參數只有一個的話,那就直接返回咯,那麼參數爲0會怎麼樣?
咱們能夠實踐下,至關於new Date(undefined),返回的結果是 Invalid Date的Date對象。
爲何能確定是Date對象呢,咱們使用instanceof Date去測試,發現結果爲true,那麼當咱們打印出來爲何爲Invalid Date呢,很明顯,它調用了 toString方法或者valueOf()方法,通過測試是toString方法,valueOf方法返回的是NaN。
好了,擴展就到這裏,繼續看下去,
有了構造函數,那麼怎麼能夠沒有原型對象呢,來了:

d3_date_utc.prototype = {
  getDate: function() {
    return this._.getUTCDate();
  ,
  getDate: function() {
    return this._.getUTCDay();
  },
  ...
}

能夠看到,D3封裝了原始Date對象的一些方法,例如getDay和GetHours等,它不適用原生的Date.getDay
等,而是使用getUTCDay去拿,那麼這二者有什麼不同嗎?
當你new一個Date對象的時候,返回的是本地的時間,注意,是你所在時區的時間哦,因此假設你如今的時間是
Tue Jul 19 2016 14:44:19 GMT+0800 (中國標準時間)
那麼當你使用getHours的時候,返回的時間是14,可是,當你使用getUTCHours的時候,它返回的是全球的時間,什麼叫全球?請參考MDN上關於這個函數的解釋:

The **getUTCHours()

** method returns the hours in the specified date according to universal time.

它的意思是會參考0時區的時間來給你時間,因爲咱們所處的地方(中國)是在8時區,因此在0時區比咱們這裏早8個小時,因此他們那邊如今仍是早晨8點正在洗臉刷牙吃早餐。

因此這個對象封裝了Date對象的UTC方法,變成一個全球流的時間器,而後它的方法再也不須要添加UTC這個名字就能夠調用了,其實咱們也能夠作到。

接下來是幾個函數的聲明和定義:

function d3_time_interval(local, step, number) {
  fucntion round(date) {}
  function ceil(date) {}
  function offset(date, k) {}
  function range(t0, t1, dt) {}
  function range_utc(t0, t1, dt) {}
    local.floor = local;
    local.round = round;
    local.ceil = ceil;
    local.offset = offset;
    local.range = range;
    var utc = local.utc = d3_time_interval_utc(local);
    utc.floor = utc;
    utc.round = d3_time_interval_utc(round);
    utc.ceil = d3_time_interval_utc(ceil);
    utc.offset = d3_time_interval_utc(offset);
    utc.range = range_utc;
    return local;
}

暫時不看這個函數裏面的函數是作什麼的,首先d3_time_interval這個函數接受三個參數,而後對傳入的local參數,咱們給了它五個方法,分別是咱們定義的五個方法,而後又給local定義個utc的屬性,這個屬性還額外擁有五個方法,最後返回了這個local對象,能夠看出來這個函數是一個包裝器,對傳入的local對象進行包裝,讓它擁有固定的方法,接下來看下一個函數:

function d3_time_interval_utc(method) {
    return function(date, k) {
      try {
        d3_date = d3_date_utc;
        var utc = new d3_date_utc();
        utc._ = date;
        return method(utc, k)._;
      } finally {
        d3_date = Date;
      }
    };
  }

一個返回函數的函數,這是在類庫裏面常常見到的用法,我常常被它給迷醉,能用的好能創造出很奇妙的做用。看代碼咱們仍然不知道具體是作什麼的,不急,繼續往下看

d3_time.year = d3_time_interval(function(date) {
    date = d3_time.day(date);
    date.setMonth(0, 1);
    return date;
  }, function(date, offset) {
    date.setFullYear(date.getFullYear() + offset);
  }, function(date) {
    return date.getFullYear();
  });

咱們知道d3_time就是d3.time對象,是一個空對象目前,這裏開始給它添加屬性了,而且調用了上面的d3_time_interval函數,向它傳入了三個函數,d3沒有註釋就是慘,徹底不知道傳入的參數類型,這點之後寫代碼須要注意

function round(date) {
      // d0是是初始化的date的本地日期,時間爲默認的凌晨或者時區時間,d1是本地時間加了一個單位,而date則相對於這兩個時間取最近的,這就是時間的round方法。
      var d0 = local(date), d1 = offset(d0, 1);
      return date - d0 < d1 - date ? d0 : d1;
    }
    // 對傳入的時間進行加一個單位
    function ceil(date) {
      step(date = local(new d3_date(date - 1)), 1);
      return date;
    }
    // 對傳入的時間作加減法
    function offset(date, k) {
      step(date = new d3_date(+date), k);
      return date;
    }

後面的一部分主要有針對傳入的參數對時間進行不一樣的格式化等等

d3.geo

d3的圖形化算法的實現,這一部分涉及到了幾何、數據結構等方面的知識,大概三千多行的代碼量,基本是各類符號和公式,沒有註釋的話看起來和天書沒有區別,須要單獨花時間來慢慢看了。

[d3.interpolate]()

接下來的是d3關於不一樣類型的插值的實現
首先是顏色:d3.interpolateRgb

d3.interpolateRgb = d3_interpolateRgb;
  function d3_interpolateRgb(a, b) {
    a = d3.rgb(a);
    b = d3.rgb(b);
    var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab;
    return function(t) {
      return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t));
    };
  }

顏色的插值實現其實沒有什麼技巧,就是分別取rgb三個值作插值,而後再將三種顏色合併爲一種顏色,之後能夠本身實現一個顏色插值器了。

除了顏色,還有對對象的插值實現:

d3.interpolateObject = d3_interpolateObject;
  function d3_interpolateObject(a, b) {
    var i = {}, c = {}, k;
    for (k in a) {
      if (k in b) {
        i[k] = d3_interpolate(a[k], b[k]);
      } else {
        c[k] = a[k];
      }
    }
    for (k in b) {
      if (!(k in a)) {
        c[k] = b[k];
      }
    }
    return function(t) {
      for (k in i) c[k] = i[k](t);
      return c;
    };
  }

遍歷兩個對象,用i存儲兩個對象都有的屬性的值的插值,用c來存儲兩個對象各自獨有的屬性值,最後合併i到c中,完事。

D3還實現了字符串的插值,不過不是對字符的插值,而是檢測字符串的數字作插值,對傳入的參數a和b,每次檢測到a中的數字,便到b中找對應的數字而後作插值,若是a的數字找不到對應,就會被拋棄,a中的其餘字符串都會被拋棄,只保留b中的字符串。

/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g

匹配數字的正則表達式

除了d3自己提供的這些插值器外,咱們也能夠自定義插值器

d3.interpolate = d3_interpolate;
  function d3_interpolate(a, b) {
    var i = d3.interpolators.length, f;
    while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ;
    return f;
  }
  d3.interpolators = [ function(a, b) {
    var t = typeof b;
    return (t === "string" ? d3_rgb_names.has(b.toLowerCase()) || /^(#|rgb\(|hsl\()/i.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === "object" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b);
  } ];

d3會本身循環遍歷插值器隊列,直到有插值器返回了對應的對象。

[d3.ease]()

d3.ease實現了多種動畫函數,開發者能夠根據自身的須要調用不一樣的動畫效果,具體的示例能夠參考這篇文章

d3.transform

d3只涉及到平面上的轉化,tranform包含四個屬性:rotate、translate、scale、skew(斜交),transform也是一個變化,因此也能夠做爲插值器,關於csstransform的文檔

相關文章
相關標籤/搜索