D3 源代碼解析(二)

對集合的操作

關于d3.attr

一個可以處理很多情況的函數(shù)坷檩,當只傳入一個參數(shù)時却音,如果是string,則返回該屬性值矢炼,如果是對象系瓢,則遍歷設置對象的鍵值對屬性值,如果參數(shù)大于等于2句灌,則是普通的設置樣式:

var node = d3.select('body')

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

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

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

那么怎么做到一個函數(shù)處理多種情況夷陋,很明顯是根據(jù)參數(shù)的數(shù)量來區(qū)別對待:

  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函數(shù)清蚀,它返回一個通用函數(shù)匕荸,該函數(shù)會對當前對象設置對應的屬性值:
大概的思想:

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

selection.classed

具體用法可以看文檔介紹,大概的意思是如果有鍵值對或者對象傳入枷邪,則根據(jù)value值來添加或刪除name類榛搔,否則則檢測是否含有該類, 如果selection有多個东揣,只檢測第一個并返回該值

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

和attr一樣践惑,通過對參數(shù)長度和類型的區(qū)分,執(zhí)行不同的方法

  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函數(shù)用來分割:

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

這里涉及到一個小細節(jié)尔觉,先用trim過濾掉字符串兩邊的空白字符,然后用正則表達式去分割類名芥吟,正則表達式中的\s匹配任何空白字符侦铜,包括空格、制表符钟鸵、換頁符等等钉稍。等價于 [ \f\n\r\t\v],而且還有一個,它在這里應該是匹配第一個的意思棺耍,測試了一下贡未,發(fā)現(xiàn)如果不加這個匹配的話,對于空白字符串不會返回長度為0的數(shù)組蒙袍,而是會返回含有一個空字符串長度為一的數(shù)組俊卤,所以這應該是為了防止出現(xiàn)這種情況而做的匹配,不過原理還是不懂害幅。對于正則的組合瘾蛋,暫時不理解加****就能防止該問題的原因。

關于匹配是否存在該類矫限,為了防止匹配的時候發(fā)生類名為’asdf',測試的類名為'a',由于包含關系而被匹配成功哺哼,所以不能簡單的使用indexOf的方法佩抹,而是要使用正則表達式去做匹配,由于類名要么在最開始取董,要么在中間兩邊有空格棍苹,要么在末尾,所以使用

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

去做正則匹配

這里用到了(?:pattern)的方法茵汰,意思是匹配 pattern 但不獲取匹配結果枢里,也就是說這是一個非獲取匹配,不進行存儲供以后使用蹂午。這在使用 "或" 字符 (|) 來組合一個模式的各個部分是很有用栏豺。例如, 'industr(?:y|ies) 就是一個比 'industry|industries' 更簡略的表達式豆胸。

d3_selectionPrototype.style

和attr結構類似的函數(shù)奥洼,特別在于如果傳入的值是函數(shù),則會分別對每個元素調(diào)用一次函數(shù)晚胡,并傳入元素和元素的位置灵奖、優(yōu)先級等

  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的實現(xiàn)方法瓷患,具體可以看看鑫大大的文章

一般我們用的是window.getComputedStyle(elem, '偽類')還有IE自娛自樂的currentStyle遣妥, 具體的細節(jié)就不說了擅编。
兩者的不同在于getPropertyValue只能獲取設置在style中的屬性,而window.getComputedStyle則會得到元素最終顯示在頁面上的綜合樣式箫踩,就算沒有顯示聲明也可以拿到沙咏,這點是最重要的區(qū)別。

selectionPrototype.property班套、 selectionPrototype.text

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

text 設置元素的文本,是通過element.textContent來設置文本的吱韭,之前我們設置文本和html都是通過innerText和innerHTML去設置吆豹,那么這和textContent有什么區(qū)別嗎?

實驗
筆者測試了下在Chrome和firefox下的情況理盆,發(fā)現(xiàn)最新版本的瀏覽器其實都是支持兩者的痘煤,不過innerText并不是w3c標準,所以以前firefox并不支持innerText猿规。

兩者的區(qū)別

  • 轉(zhuǎn)義上衷快,textContent對傳入的文本如果帶有\(zhòng)n等換行符,不會忽略姨俩,而innText會忽略并轉(zhuǎn)義為空格
  • textContent會獲取所有子節(jié)點的文本蘸拔,而innerText不會理會隱藏節(jié)點的文本师郑。

selectionProperty.html

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

d3_selectionPrototype.append

比較特別的是實現(xiàn)的代碼:

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

函數(shù)中返回一個函數(shù)的執(zhí)行結果调窍,該執(zhí)行函數(shù)中又返回一個函數(shù)的執(zhí)行結果宝冕,層層嵌套卻又非常聰明的做法,我們從最里面的一層看邓萨,首先對當前的節(jié)點添加子元素地梨,然后返回該子節(jié)點元素,最后再通過select方法獲取該子元素缔恳。

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

這是簡易版本的creator宝剖,d3還要考慮到在xml中的情況,xml創(chuàng)建子節(jié)點調(diào)用的是document.createElementNS歉甚,d3是通過namespaceURI來判斷頁面類型的吧万细,不過在MDN上查詢發(fā)現(xiàn)這個屬性已經(jīng)被列為廢詞,隨時可能被廢除的铃芦,查詢了版本4雅镊,發(fā)現(xiàn)還是沿用了這個屬性襟雷,這個比較危險吧刃滓。

d3_selectionPrototype.insert && d3_selectionPrototype.remove

** insertBefore**
同append類似,不過是封裝了insertBefore的方法耸弄,注意需要用元素節(jié)點才能調(diào)用該方法咧虎,正確的調(diào)用方法是:
existNodeParents.insertBefore(newNode, existNodeToBeInsertBefore)
** remove**
很簡單的實現(xiàn):

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

Data

關于d3_selectionPrototype.data函數(shù)

這個函數(shù)是D3經(jīng)常使用到也是比較關鍵的函數(shù),用它來進行數(shù)據(jù)的綁定计呈、更新砰诵,具體解析可以參考上一篇文章D3源代碼解構
這里涉及到一個特殊的屬性data,如果不傳入?yún)?shù),data會返回所有算中集合元素的屬性值(property)捌显,但是為什么是通過node.data拿到的茁彭,通過搜索,終于找到了綁定該值得函數(shù)(一開始還以為是DOM的隱藏變量- -)

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

如果傳入?yún)?shù)扶歪,它會創(chuàng)建三個特殊的私有變量理肺,分別是

  • enter = d3_selection_enter([])
  • update = d3_selection([])
  • exit = d3_selection([])
    我們可以知道update和exit都是一個繼承了d3_selectionPrototype原型對象的數(shù)組,所以它擁有我們上面提到的selectionPrototype所有的方法善镰,而enter比較特殊妹萨,它單獨使用一套原型方法,實現(xiàn)方法如下:
  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);
  };

然后調(diào)用bind函數(shù)對傳入的data和key(可選)進行數(shù)據(jù)綁定炫欺,我們知道d3會根據(jù)傳入的數(shù)據(jù)和已有的元素進行一一對應乎完,一開始以為是基于什么算法去對應,看代碼實現(xiàn)就發(fā)現(xiàn)如果我們不傳入key參數(shù)品洛,其實就是簡單的索引對應:

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會根據(jù)我們傳入的這個函數(shù)去將元素和數(shù)據(jù)做綁定和更新、退出娃弓,這個key函數(shù)會在三次循環(huán)中分別被調(diào)用典格,一次是檢查是否有已經(jīng)綁定了數(shù)據(jù)的元素,并初始化一個映射集合台丛,第二次進行數(shù)據(jù)綁定元素耍缴,確定update和enter集合,第三次確定exit集合挽霉。
建議先看看官方文檔防嗡,了解具體的用法在看代碼會清晰很多。通俗的說侠坎,假設我們傳入的數(shù)據(jù)有主鍵即唯一區(qū)分每個數(shù)據(jù)的屬性蚁趁,那么,我們便可以告訴data說用這個屬性來區(qū)分实胸,也就是:

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

關于d3_map集合可以參考d3_map解析

Animation & Interaction (動畫和交互)

d3_selectionPrototype.datum

這是上面講到的一個函數(shù)datum他嫡,可惜在data中其實沒有用到,我遍歷了整個代碼只有一處地方調(diào)用了這個函數(shù)庐完,它和data類似用來獲取或者設置元素的值钢属,它是基于property上進行一層封裝,但是和data不同的是它沒有所謂的enter门躯、exit集合返回淆党,那么它有什么用呢?我們可以看看這篇文章

d3_selectionPrototype.filter

可以傳入函數(shù)或者選擇器字符串進行集合的過濾

d3的事件監(jiān)聽機制

看d3關于事件監(jiān)聽的實現(xiàn)讶凉,看到了關于JS事件的一個屬性relatedTarget染乌,關于JS的event對象之前接觸的不多,突然看到關于這個屬性懂讯,上網(wǎng)查找資料荷憋,才發(fā)現(xiàn)了這么冷門的屬性:

relatedTarget 事件屬性返回與事件的目標節(jié)點相關的節(jié)點。
對于 mouseover 事件來說褐望,該屬性是鼠標指針移到目標節(jié)點上時所離開的那個節(jié)點勒庄。
對于 mouseout 事件來說,該屬性是離開目標時譬挚,鼠標指針進入的節(jié)點锅铅。
對于其他類型的事件來說,這個屬性沒有用减宣。

怎么樣盐须,夠冷門吧,只對兩種事件生效

還有一個方法叫做compareDocumentPosition漆腌,比較兩個節(jié)點贼邓,并返回描述它們在文檔中位置的整數(shù)
1:沒有關系阶冈,兩個節(jié)點不屬于同一個文檔。
2:第一節(jié)點(P1)位于第二個節(jié)點后(P2)塑径。
4:第一節(jié)點(P1)定位在第二節(jié)點(P2)前女坑。
8:第一節(jié)點(P1)位于第二節(jié)點內(nèi)(P2)。
16:第二節(jié)點(P2)位于第一節(jié)點內(nèi)(P1)统舀。
32:沒有關系匆骗,或是兩個節(jié)點是同一元素的兩個屬性。
注釋:返回值可以是值的組合誉简。例如碉就,返回 20 意味著在 p2 在 p1 內(nèi)部(16),并且 p1 在 p2 之前(4)闷串。

知道了這兩個屬性瓮钥,d3的一個函數(shù)就看懂了:

  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);
      }
    };
  }

獲取事件對應的對象和相關的對象,如果不存在相關的對象或者相關的對象不等于當前對象且相關對象不在當前對象之內(nèi)烹吵,則執(zhí)行監(jiān)聽函數(shù)碉熄。

  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;
      }
    };
  }

這個函數(shù)返回一個函數(shù),返回的函數(shù)綁定了當前對象并執(zhí)行肋拔。

  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只损,然后判斷環(huán)境中是否有這兩個事件一姿,如果有的話就取消這個映射七咧。

以上三段代碼都是為了處理執(zhí)行環(huán)境中沒有mouseenter和mousemove情況下如何利用mouseover和mouseleave去實現(xiàn)相同效果的問題跃惫。然后通過下面這個函數(shù)來判斷:

  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;
  }

現(xiàn)在再來看這個函數(shù)就可以看懂了,首先它判斷傳入的事件類型是否含有'.'艾栋,因為D3在實現(xiàn)事件綁定時爆存,會清除同種事件類型之前綁定的監(jiān)聽函數(shù),所以對于同一類型的事件蝗砾,如果要綁定多個監(jiān)聽函數(shù)先较,那么就需要使用click.foo*click.bar*這種方式去進行區(qū)分,防止舊的事件被覆蓋掉悼粮,查看onAdd函數(shù)就可以知道每次添加事件監(jiān)聽的時候闲勺,就會調(diào)用onRemove去清除該事件監(jiān)聽。

關于capture扣猫,默認是false菜循,表示在冒泡階段響應事件,如果設置為true申尤,則是在捕獲階段響應事件癌幕,可以參考這篇文章衙耕,這是歷史遺留原因,好像當初的瀏覽器響應事件的設置不是冒泡階段勺远,而是捕獲階段橙喘,后來為了兼容而給了這個參數(shù)。

好了胶逢,懂得了D3事件綁定的原理厅瞎,那么實現(xiàn)這個函數(shù)就很容易,一樣的根據(jù)參數(shù)的數(shù)量和類型做不同的處理就好了:

  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坐標轉(zhuǎn)換為屏幕坐標
關于使用矩陣轉(zhuǎn)換的實現(xiàn)
我們要知道一些新的屬性:

  • ownerSVGElement】初坠,用來獲取這個元素最近的svg祖先磁奖,沒有的話就返回元素本身。
  • svg.createSVGPoint】這個函數(shù)不在MDN中某筐,看下MF的介紹比搭,大概意思是初始化一個不在document文檔內(nèi)的坐標點
  • getScreenCTM

當我們獲取網(wǎng)頁上鼠標的坐標點的時候,可以很簡單地調(diào)用e.clientXY,或者e.pageXY南誊,但是svg有自己的一套坐標系身诺,它可以自身旋轉(zhuǎn)、平移抄囚,所以我們想知道按鈕點擊的位置相對于svg元素的位置時霉赡,需要考慮這些因素,從而使得獲取鼠標在svg的位置時變得沒那么容易幔托,再加上各種瀏覽器的坑……
這個時候就是線性代數(shù)就用上了(感謝線代老師Q鳌),忘的差不多的可以參考上面的幾篇文章重挑,svg自身已經(jīng)提供了對應的矩陣運算嗓化,節(jié)省了我們的一些實現(xiàn)的代碼。
再看看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

看不太懂這幾個的實現(xiàn)悍手,和自己沒有怎么使用到這幾個函數(shù)有關吧

d3.zoom

zoom函數(shù)的實現(xiàn),大概知道它通過綁定mouseWheel事件去記錄了放縮的值、中心谓苟、放縮位置等官脓。也是涉及到event的綁定,表示hin暈涝焙。

D3的顏色空間

具體可以參考前一篇文章

d3.xhr

D3對于ajax的實現(xiàn)卑笨,沒有兼容IE6及6以下的xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
只考慮了window.XMLHttpRequest,因為老版本的IE壓根就無法正常使用各種圖形和動畫。

D3的timer的實現(xiàn)有點厲害

當我們要用D3實現(xiàn)一個永久循環(huán)的動畫的時候仑撞,就可以使用timer函數(shù)赤兴,向這個函數(shù)傳入一個函數(shù),timer函數(shù)會在每個動畫針中調(diào)用傳入的函數(shù)直至該函數(shù)返回‘true’隧哮,所以只要我們始終不返回true就好了桶良。
如果是這么簡單當然就好實現(xiàn)了,但是如果有多個timer怎么去控制呢沮翔?這個問題導致了實現(xiàn)的方法復雜了很多陨帆,直接上代碼:

  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);
    }
  }
  // 立即執(zhí)行時間隊列,然后清洗掉已經(jīng)結束的事件采蚀。
  d3.timer.flush = function() {
    d3_timer_mark();
    d3_timer_sweep();
  };
  // 遍歷時間隊列疲牵,如果回調(diào)函數(shù)返回真,則將該事件的回調(diào)賦值為空榆鼠,然后繼續(xù)檢查下一個纲爸,最后返回當前時間。
  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;
  }
  // 時間事件隊列的清洗妆够,循環(huán)遍歷隊列中的時間對象识啦,如果回調(diào)函數(shù)為空,去掉神妹,否則檢測下一個颓哮,最后返回最近要執(zhí)行的事件時間點。
  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使用隊列的方法實現(xiàn)灾螃,每次有新的timer進來题翻,判斷隊列是否為空揩徊,如果為空腰鬼,就將Head和隊尾指向它,否則塑荒,將隊尾和隊尾的下一個指向它

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

感謝C和C++熄赡,告訴我指針實現(xiàn)鏈表的概念!

然后開始執(zhí)行回調(diào)函數(shù)齿税。

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

timer_frame的實現(xiàn)是兼容了老版本的瀏覽器沒有** requestAnimationFrame** 而退而使用setTimeout去實現(xiàn)彼硫,如果不太清楚這個api的同學可以看看鑫旭的這篇文章或者上MDN查。
然后每個幀都會調(diào)用d3_timer_step這個函數(shù),它調(diào)用了d3_timer_mark和d3_timer_sweep函數(shù)拧篮,循環(huán)遍歷了一遍時間隊列词渤,然后獲取最近的待執(zhí)行的時間點,得到了delay時間差爽丹,當時間差大于24并且不為Infinity的時候倦畅,便重新設置時間器抽诉,讓其在delay ms后執(zhí)行,減少性能的消耗高氮,若為Infinity,表示沒有時間事件等待調(diào)用顷牌,停止了遞歸剪芍,否則,delay小于24ms窟蓝,遞歸調(diào)用d3_timer_frame罪裹。

那么為什么為24ms呢?我們知道瀏覽器的最佳動畫幀是60fbps运挫,算起來每一幀的間隔為1000/60 = 16.7ms,所以如果使用setTimeout實現(xiàn)動畫針的話坊谁,d3選擇的時間間隔是17ms,因為太小的話會出現(xiàn)掉幀的情況滑臊,那么這個和24有什么關系呢口芍?為什么要設定為24呢?我也不清楚...在github上面提交了issues雇卷,不知道會不會有人解答鬓椭,好緊張。
關于timer的一些擴展:
timer實現(xiàn)永久動畫
作者的實現(xiàn)

早上提交的issue下午原作者就給了回復关划,不過作者的解釋就尷尬了小染,大概的意思就是由于setTimeout的不穩(wěn)定和不準確,存在一定的延遲贮折,所以在設定這個值的時候也是拍腦袋設置的裤翩,值剛好在16.7到33.4之間,并回復說左右偏移都不會有什么影響就對了调榄。

d3關于number 的方法:formatPrefix 和 round

提供了將number轉(zhuǎn)化為特定格式的字符串方法踊赠,基于正則表達做匹配,然后對應地做轉(zhuǎn)化每庆。這部分的實現(xiàn)比較瑣碎筐带,就沒去仔細研究了,有興趣的可以看看缤灵。

d3.time

同樣的伦籍,將d3.time初始化為一個空對象蓝晒,并且將window.Date對象設置為私有變量:d3_date = Date
萬物皆為我所用!
首先我們要了解Date的UTC函數(shù)帖鸦,UTC() 方法可根據(jù)世界時返回 1970 年 1 月 1 日 到指定日期的毫秒數(shù)芝薇。
然后來看這個函數(shù):

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

這個函數(shù)是一個構造函數(shù),當我們new d3_date_utc(xxx)的時候作儿,它會創(chuàng)建一個日期對象剩燥,并根據(jù)我們傳入的參數(shù)數(shù)量去創(chuàng)建,如果我們傳入的參數(shù)多余1個立倍,那么很顯然我們傳入的是年月日這些參數(shù)灭红,那么便調(diào)用** Date.UTC.apply**去返回時間戳,如果參數(shù)只有一個的話口注,那就直接返回咯变擒,那么參數(shù)為0會怎么樣?
我們可以實踐下寝志,相當于new Date(undefined),返回的結果是 Invalid Date的Date對象娇斑。
為什么能肯定是Date對象呢,我們使用instanceof Date去測試材部,發(fā)現(xiàn)結果為true毫缆,那么當我們打印出來為什么為Invalid Date呢,很明顯乐导,它調(diào)用了 toString方法或者valueOf()方法苦丁,經(jīng)過測試是toString方法,valueOf方法返回的是NaN物臂。
好了旺拉,擴展就到這里,繼續(xù)看下去棵磷,
有了構造函數(shù)蛾狗,那么怎么可以沒有原型對象呢,來了:

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

可以看到仪媒,D3封裝了原始Date對象的一些方法沉桌,例如getDay和GetHours等,它不適用原生的Date.getDay
等算吩,而是使用getUTCDay去拿留凭,那么這兩者有什么不一樣嗎?
當你new一個Date對象的時候赌莺,返回的是本地的時間冰抢,注意,是你所在時區(qū)的時間哦艘狭,所以假設你現(xiàn)在的時間是
Tue Jul 19 2016 14:44:19 GMT+0800 (中國標準時間)
那么當你使用getHours的時候,返回的時間是14,但是巢音,當你使用getUTCHours的時候遵倦,它返回的是全球的時間,什么叫全球官撼?請參考MDN上關于這個函數(shù)的解釋:

The **getUTCHours()
** method returns the hours in the specified date according to universal time.

它的意思是會參考0時區(qū)的時間來給你時間梧躺,由于我們所處的地方(中國)是在8時區(qū),所以在0時區(qū)比我們這里早8個小時傲绣,所以他們那邊現(xiàn)在還是早晨8點正在洗臉刷牙吃早餐掠哥。

所以這個對象封裝了Date對象的UTC方法,變成一個全球流的時間器秃诵,然后它的方法不再需要添加UTC這個名字就可以調(diào)用了续搀,其實我們也可以做到。

接下來是幾個函數(shù)的聲明和定義:

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;
}

暫時不看這個函數(shù)里面的函數(shù)是做什么的菠净,首先d3_time_interval這個函數(shù)接受三個參數(shù)禁舷,然后對傳入的local參數(shù),我們給了它五個方法毅往,分別是我們定義的五個方法牵咙,然后又給local定義個utc的屬性,這個屬性還額外擁有五個方法攀唯,最后返回了這個local對象洁桌,可以看出來這個函數(shù)是一個包裝器,對傳入的local對象進行包裝侯嘀,讓它擁有固定的方法战坤,接下來看下一個函數(shù):

  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;
      }
    };
  }

一個返回函數(shù)的函數(shù),這是在類庫里面經(jīng)常見到的用法残拐,我經(jīng)常被它給迷醉途茫,能用的好能創(chuàng)造出很奇妙的作用∠常看代碼我們?nèi)匀徊恢谰唧w是做什么的囊卜,不急,繼續(xù)往下看

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對象错沃,是一個空對象目前栅组,這里開始給它添加屬性了,并且調(diào)用了上面的d3_time_interval函數(shù)枢析,向它傳入了三個函數(shù)玉掸,d3沒有注釋就是慘,完全不知道傳入的參數(shù)類型醒叁,這點以后寫代碼需要注意

    function round(date) {
      // d0是是初始化的date的本地日期司浪,時間為默認的凌晨或者時區(qū)時間泊业,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;
    }

后面的一部分主要有針對傳入的參數(shù)對時間進行不同的格式化等等

d3.geo

d3的圖形化算法的實現(xiàn),這一部分涉及到了幾何租谈、數(shù)據(jù)結構等方面的知識篮奄,大概三千多行的代碼量,基本是各種符號和公式割去,沒有注釋的話看起來和天書沒有區(qū)別窟却,需要單獨花時間來慢慢看了。

d3.interpolate

接下來的是d3關于不同類型的插值的實現(xiàn)
首先是顏色: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));
    };
  }

顏色的插值實現(xiàn)其實沒有什么技巧呻逆,就是分別取rgb三個值做插值夸赫,然后再將三種顏色合并為一種顏色,以后可以自己實現(xiàn)一個顏色插值器了页慷。

除了顏色憔足,還有對對象的插值實現(xiàn):

  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還實現(xiàn)了字符串的插值揭绑,不過不是對字符的插值,而是檢測字符串的數(shù)字做插值郎哭,對傳入的參數(shù)a和b他匪,每次檢測到a中的數(shù)字,便到b中找對應的數(shù)字然后做插值夸研,如果a的數(shù)字找不到對應邦蜜,就會被拋棄,a中的其他字符串都會被拋棄亥至,只保留b中的字符串悼沈。

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

匹配數(shù)字的正則表達式

除了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會自己循環(huán)遍歷插值器隊列姐扮,直到有插值器返回了對應的對象絮供。

d3.ease

d3.ease實現(xiàn)了多種動畫函數(shù),開發(fā)者可以根據(jù)自身的需要調(diào)用不同的動畫效果茶敏,具體的示例可以參考這篇文章

d3.transform

d3只涉及到平面上的轉(zhuǎn)化壤靶,tranform包含四個屬性:rotate、translate惊搏、scale贮乳、skew(斜交)忧换,transform也是一個變化,所以也可以作為插值器塘揣,關于csstransform的文檔

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末包雀,一起剝皮案震驚了整個濱河市宿崭,隨后出現(xiàn)的幾起案子亲铡,更是在濱河造成了極大的恐慌,老刑警劉巖葡兑,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奖蔓,死亡現(xiàn)場離奇詭異,居然都是意外死亡讹堤,警方通過查閱死者的電腦和手機吆鹤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洲守,“玉大人疑务,你說我怎么就攤上這事」4迹” “怎么了知允?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長叙谨。 經(jīng)常有香客問我温鸽,道長,這世上最難降的妖魔是什么手负? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任涤垫,我火速辦了婚禮,結果婚禮上竟终,老公的妹妹穿的比我還像新娘蝠猬。我一直安慰自己,他們只是感情好统捶,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布榆芦。 她就那樣靜靜地躺著,像睡著了一般瘾境。 火紅的嫁衣襯著肌膚如雪歧杏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天迷守,我揣著相機與錄音犬绒,去河邊找鬼。 笑死兑凿,一個胖子當著我的面吹牛凯力,可吹牛的內(nèi)容都是我干的茵瘾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼咐鹤,長吁一口氣:“原來是場噩夢啊……” “哼拗秘!你這毒婦竟也來了?” 一聲冷哼從身側響起祈惶,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤雕旨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捧请,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凡涩,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年疹蛉,在試婚紗的時候發(fā)現(xiàn)自己被綠了活箕。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡可款,死狀恐怖育韩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闺鲸,我是刑警寧澤筋讨,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站翠拣,受9級特大地震影響版仔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜误墓,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一蛮粮、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谜慌,春花似錦然想、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恼琼,卻和暖如春妨蛹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晴竞。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工蛙卤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓颤难,卻偏偏與公主長得像神年,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子行嗤,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 國家電網(wǎng)公司企業(yè)標準(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,005評論 6 13
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理已日,服務發(fā)現(xiàn),斷路器栅屏,智...
    卡卡羅2017閱讀 134,704評論 18 139
  • 當明天變成了今天成為了昨天 最后成為記憶里不再重要的某一天 我們突然發(fā)現(xiàn)自己在不知不覺中已被時間推著向前走 這不是...
    太陽笑瞇瞇閱讀 448評論 14 24
  • 中國的西沙群島 一個比馬爾代夫更美的地方 西沙群島既琴,它雖然屬于我們的國土占婉。但是大家對其知之甚少泡嘴,從去年十月開始西沙...
    一諾0001閱讀 723評論 0 0
  • 對戶外茶會有一種天然的喜歡甫恩。 沒有比天地更美的茶席了。 何況還有四季變幻的風景當作掛畫酌予,各種野花隨手掐來當茶花呢!...
    木春閱讀 243評論 4 0