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 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 d3_selectionPrototype.style
和attr结构类似的函数,特别在于如果传入的值是函数,则会分别对每个元素调用一次函数,并传入元素和元素的位置、优先级等

  d3_selectionPrototype.style = function(name, value, priority) {    var n = arguments.length;    if (n 文章,一般我们用的是window.getComputedStyle(elem, '伪类')还有IE自娱自乐的currentStyle, 具体的细节就不说了。两者的不同在于getPropertyValue只能获取设置在style中的属性,而window.getComputedStyle则会得到元素最终显示在页面上的综合样式,就算没有显示声明也可以拿到,这点是最重要的区别。### selectionPrototype.property、 selectionPrototype.textproperty 给元素设置额外的属性,例如:node.property('bar', 'hahahaha')node.property('bar') // hahahahatext 设置元素的文本,是通过element.textContent来设置文本的,之前我们设置文本和html都是通过innerText和innerHTML去设置,那么这和textContent有什么区别吗?**实验**笔者测试了下在Chrome和firefox下的情况,发现最新版本的浏览器其实都是支持两者的,不过innerText并不是w3c标准,所以以前firefox并不支持innerText。**两者的区别**1. 转义上,textContent对传入的文本如果带有n等换行符,不会忽略,而innText会忽略并转义为空格1. 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");
};

如果传入参数,它会创建三个特殊的私有变量,分别是1. enter  = d3_selection_enter([])1. update = d3_selection([])1. 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 文章

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 MDN上关于svg的一些属性一篇关于svg的讲解关于svg坐标转换为屏幕坐标.aspx)关于使用矩阵转换的实现我们要知道一些新的属性:1. 【**ownerSVGElement**】,用来获取这个元素最近的svg祖先,没有的话就返回元素本身。1. 【**svg.createSVGPoint**】这个函数不在MDN中,看下MF的介绍.aspx),大概意思是初始化一个不在document文档内的坐标点1. 【**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 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  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 文章或者上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_dateutc() {
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_dateutc.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_dateutc();
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 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的文档

关键字:JavaScript, d3.js, 源码分析

版权声明

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部