D3 源代码剖析(二)

这是继上一篇D3源码解构文章后的对D3的研讨笔记,笔者的才有限,若有那里明白毛病,迎接斧正。

对鸠合的操纵

关于d3.attr

一个能够处置惩罚许多状况的函数,当只传入一个参数时,假如是string,则返回该属性值,假如是对象,则遍历设置对象的键值对属性值,假如参数大于即是2,则是平常的设置款式:

var node = d3.select('body')

node.attr('class')
> 返回该属性值

node.attr('class', 'haha')
> 设置该属性值

node.attr({'class': 'haha', 'x': '10'})
> 设置该属性值

那末怎样做到一个函数处置惩罚多种状况,很明显是依据参数的数目来区分对待:

  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函数,它返回一个通用函数,该函数会对当前对象设置对应的属性值:
也许的头脑:

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

selection.classed

详细用法能够看文档引见,也许的意义是假如有键值对或许对象传入,则依据value值来增加或删除name类,不然则检测是不是含有该类, 假如selection有多个,只检测第一个并返回该值

var line = d3.selectAll('line');
line.classed('a b c d', true)
>对一切节点设置class
line classed({'a': true, 'b': false})
>离别增加和删除类

和attr一样,经由过程对参数长度和范例的辨别,实行差别的要领

  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函数用来支解:

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

这里触及到一个小细节,先用trim过滤掉字符串双方的空缺字符,然后用正则表达式去支解类名,正则表达式中的s婚配任何空缺字符,包括空格、制表符、换页符等等。等价于 [ fnrtv],而且另有一个^,它在这里应当是婚配第一个的意义,测试了一下,发明假如不加这个婚配的话,关于空缺字符串不会返回长度为0的数组,而是会返回含有一个空字符串长度为一的数组,所以这应当是为了防备涌现这类状况而做的婚配,不过道理照样不懂。关于正则的组合,临时不明白加^就可以防备该题目的缘由。

关于婚配是不是存在该类,为了防备婚配的时刻发作类名为’asdf’,测试的类名为’a’,由于包括关联而被婚配胜利,所以不能简朴的运用indexOf的要领,而是要运用正则表达式去做婚配,由于类名要么在最最先,要么在中间双方有空格,要么在末端,所以运用

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

去做正则婚配

这里用到了(?: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的文档

    原文作者:chenjsh36
    原文地址: https://segmentfault.com/a/1190000006134016
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞