d3.js学习笔记(2)—— Transition
来源:互联网 发布:数控编程 过程 编辑:程序博客网 时间:2024/06/09 21:20
今天是纪录学习d3的第二篇博客,主要还是从源码的角度来分析d3的transition是如何实现的。这篇文章可能比较长,不过如果能够读完的话,我觉得肯定会对d3有更加深刻的认识。
学习资料
和上一篇一样首先列一下学习的资料,这些都能够在d3的github上能够找到
- 官方api
- Working with Transitions
- Creating Animations and Transitions With D3
- General Update Pattern, III
- d3源代码
简单的demo
我们先从一个简单的demo来熟悉一下d3中transition的api
效果很简单就是一个小球在移动呗。
html代码
<svg class = 'world' width = '600' height = '400' version = "1.1" baseProfile = "full" xmlns="http://www.w3.org/2000/svg"> <circle class = 'cirlce' cx = '30' cy = '30' r = '20' fill = 'red'> </circle></svg>
可以看到非常的简单就是定义一个svg和一个circle元素呗
javascript代码
<script type="text/javascript"> (function(d3){ setInterval(function(){ var circle; circle = d3.select('.cirlce'); circle .transition('position') .attr('cx', 570) .duration(1000) .ease('bounce') .transition('position') .attr('cy', 370) .transition('position') .attr('cx', 30) .ease('cubic') .transition('position') .attr('cy', 30) .ease('bounce'); circle .transition('size') .attr('r', 40) .duration(1000) .ease('bounce') .transition('size') .attr('r', 20) .duration(1000) .transition('size') .attr('r', 40) .duration(1000) .transition('size') .attr('r', 20) .duration(1000) }, 4200); })(d3);</script>
代码也简单到不要不要的,这里我就不解释了,不过这里有三个地方是需要注意的:
transition函数返回的东西和select返回的东西是不一样的
这里我们定义了两种transition,一种为position,一种是size
我们这里在circle调用transition后通过链式调用再次调用了transition
第一点,transition函数返回对象的类型为d3_transitionPrototype,select函数返回对象类型为d3_selectionPrototype,两者api很相似,但是确实是不一样的东西。
第二点需要注意是因为在d3中,在一个时刻,每一个元素对于一个名字(上面提到的position和size)只能执行一个动画。如果想要在一个时刻对一个元素执行多个动画,就可以像我们demo一样,给元素定义两个不同名字的动画。
第二点其实就是链式调用,因为transition函数返回的对象类型为d3_transitionPrototype,这个对象类型的transition函数所产生的动画是在当前动画执行完之后才会执行。并且新创建的动画会继承d3_selectionPrototype对象创建的transition的一些属性,比如ease,duration和delay等。对于d3_selectionPrototype的transition函数和d3_selectionPrototype的transition函数在后面我们都会进行详细的解释。
源码解析
d3.timer
想要了解d3的transition,我们必须首先了解一下d3.timer,因为d3的transition是基于d3.timer来实现的,对于它的官网解释可以看这个文档d3.timer。按照我的理解,其实d3.timer就是来维护一个动画队列的。我们来看看它的源码吧:
d3.timer = function() { d3_timer.apply(this, arguments); };
这里调用了d3_timer,我们就看看d3_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);};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;}
第1~3行代码定义了一些变量,d3_timer_queueHead队列头部,d3_timer_queueTail队列尾部,d3_timer_interval队列中是否有动画正在在执行,d3_timer_timeout用来保存setTimeout函数返回的变量,用于clearTimeout。d3_timer_frame用来执行动画的驱动函数,如果浏览器支持requestAnimationFrame,就用requestAnimationFrame,如果不支持就只能用setTimeout来模拟。
d3_timer函数很简单,首先将要执行的动画放在队列的最后,动画在队列中的表现形式如下:
timer = { c: callback, t: time, n: null}
timer.c表示每一帧要执行的函数,如果函数返回true表示该动画执行结束
timer.t表示动画执行的开始时间
timer.n指向队列中的下个动画
在将动画添加到队列后,如果当前队列中没有动画正在执行的话,就看看当前队列中是否有要执行的动画,如果有的话就执行,我觉得作者这样做的目的是为了驱动新添加动画的执行(如果必要的话)。主要是通过调用d3_timer_step。最后d3_timer返回的那个timer非常的重要,因为这里是引用变量,所以在外界可以通过返回的timer修改timer.c函数,从而改变要执行的动画的行为。
d3_timer_step函数调用了两个函数,第一个函数为d3_timer_mark,这函数执行动画队列中的每个需要执行的动画(时间到了的动画),然后将判断当前动画是否执行结束,通过timer.c函数的返回值,如果结束,将timer.c置为null,方便将该动画从队列中清楚,最后这个函数返回动画队列开始执行的时间;第二个函数是d3_timer_sweep是用来清除队列中已经执行完的动画,然后返回清除后队列中最近需要执行动画的时间。
d3_timer_step后面的代码也很简单,判断已经清除过队列中最近要执行动画的时间和当前时间的差,如果大于24ms则通过setTimeout在计算到的时间差,重新调用d3_timer_step来驱动动画的执行,如果小于等于24ms则通过d3_timer_frame调用d3_timer_step来驱动下一个动画帧周期(队列中需要执行的动画执行一次)的执行。这里需要注意的一个地方是,这里是通过d3_timer_frame来调用d3_timer_step,而不是直接调用d3_timer_step,这一点在d3的文档中也解释到了,如果在一个动画帧周期执行完之后,只剩下队列尾部的动画,那么如果直接调用d3_timer_step来执行动画的话,那么会导致一个动画同时执行两次,会有闪烁的问题,因为d3_timer_frame是异步的,有一定的延迟,一般为17ms,所以可以很好的解决这个问题。
在这里我们就比较完整的解释了一遍d3.timer,激动人心的时刻来了,我们去去看看d3的transition是如何实现的吧,毕竟我是因为被d3强大的动画能力吸引才学的。
transition
d3有两种方式来创建transition(不包括subtransition,也就是在通过transition创建的transition)。一种是通过d3.transition函数,另一种是selection.transition函数。其实实质上这里只有一种,d3.transition在函数内部调用的是selection.transition,所以这里我们只需要分析selection.transition函数就好了。
selection.transition
d3_selectionPrototype.transition = function(name) { var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || { time: Date.now(), ease: d3_ease_cubicInOut, delay: 0, duration: 250 }; for (var j = -1, m = this.length; ++j < m; ) { subgroups.push(subgroup = []); for (var group = this[j], i = -1, n = group.length; ++i < n; ) { if (node = group[i]) d3_transitionNode(node, i, ns, id, transition); subgroup.push(node); } } return d3_transition(subgroups, ns, id);};
第1~5行代码创建了transition的一些参数,由于d3_transitionInheritId和d3_transitionInherit使用来创建subtransition的,所以现在我们可以暂且忽略,在后面会有比较详细的解释的。
这里有个地方需要注意的就是第一行创建的ns,这个是一个字符串类型的变量,它是通过我们传过来的name参数来创建的,如果name为空,为__transition__
,反之为'__transition__' + name + '__'
,这个主要使用来在一个元素同时执行不同的动画的,动画执行的参数都通过ns保存在element中(Element[ns] = {...}
)
8~14行代码比较简单,就是对于selection中的每个元素调用d3_transitionNode,所以这个参数是重点,我们需要好好的分析一下:
function d3_transitionNode(node, i, ns, id, inherit) { var lock = node[ns] || (node[ns] = { active: 0, count: 0 }), transition = lock[id], time, timer, duration, ease, tweens; function schedule(elapsed) { var delay = transition.delay; timer.t = delay + time; if (delay <= elapsed) return start(elapsed - delay); timer.c = start; } function start(elapsed) { var activeId = lock.active, active = lock[activeId]; if (active) { active.timer.c = null; active.timer.t = NaN; --lock.count; delete lock[activeId]; active.event && active.event.interrupt.call(node, node.__data__, active.index); } for (var cancelId in lock) { if (+cancelId < id) { var cancel = lock[cancelId]; cancel.timer.c = null; cancel.timer.t = NaN; --lock.count; delete lock[cancelId]; } } timer.c = tick; d3_timer(function() { if (timer.c && tick(elapsed || 1)) { timer.c = null; timer.t = NaN; } return 1; }, 0, time); lock.active = id; transition.event && transition.event.start.call(node, node.__data__, i); tweens = []; transition.tween.forEach(function(key, value) { if (value = value.call(node, node.__data__, i)) { tweens.push(value); } }); ease = transition.ease; duration = transition.duration; } function tick(elapsed) { var t = elapsed / duration, e = ease(t), n = tweens.length; while (n > 0) { tweens[--n].call(node, e); } if (t >= 1) { transition.event && transition.event.end.call(node, node.__data__, i); if (--lock.count) delete lock[id]; else delete node[ns]; return 1; } } if (!transition) { time = inherit.time; timer = d3_timer(schedule, 0, time); transition = lock[id] = { tween: new d3_Map(), time: time, timer: timer, delay: inherit.delay, duration: inherit.duration, ease: inherit.ease, index: i }; inherit = null; ++lock.count; }}
这段代码虽然只有79行代码,可是这个确实transition的核心。
2~4行声明了一些变量,lock就是上面我们说的保存在element中关于transition的参数,lock.active表示该元素指定ns名字下正在运行动画的id,count表示ns名字下动画的数目。lock[id]
是用来获取和当前id对应transition参数的。当然如果之前没有绑定的话,肯定为undefined了,一般情况都是这样,因为每次创建transition的时候id都会增加1,但是在创建subtransition的时候就不一样了,因为subtransition继承了创建者transition的id,所以这里就可能重复了。
紧接着是三个函数schedule、start和tick的创建,我们先不管,直接看64~78行代码,我们可以看到如果不是创建subtransition的话,那么transition为undefined,所以就会执行if里面的代码,我们可以看到这里就是配置了一下tansition,然后将其付给lock[id]。并调用了d3_timer函数在动画队列中添加了一个动画,然后将返回值保存在了timer变量中,我们需要注意d3_timer安排的动画是异步调用的,所以在schedule函数被调用的时候transition已经被赋值了。这里需要注意一个地方就是schedule、start和tick三个函数都会访问d3_transitionNode函数中定义的一些变量,你可能觉得这没什么就是简单的闭包呗,有什么好注意的,因为每个函数在调用的时候会在作用域前面添加一个行创建的变量,这个变量保存了函数定义的变量,所以schedule、start和tick三个函数访问的d3_transitionNode定义的变量都是独一无二的,不相互被共享。
之后我们可以看到在下一个动画帧周期执行时,如果该动画可以执行的话,会调用schedule函数,我们看看这个函数里面都做了些什么,首先修正了timer中关于动画要开始执行的时间,之后判断动画是否可以执行,如果可以执行就调用start执行动画;如果没有到时间的话,那么就不需要执行start函数,然后将start函数赋给timer.c。这个是非常重要的,因为在下一个动画帧周期如果该动画能执行的话,那么调用的就是schedule函数了,而是start函数了。
我们来看看start函数,首先获取该元素ns名下是否已经有动画正在执行,如果有的话,因为现在添加的transition肯定要比那个正在执行创建要晚,所以这里需要interrupt现在正在执行的动画,并且通知调用注册的interrupt事件的回调函数。23~31行代码是取消那些在队列中还没有执行的动画,但是在相同ns下id比当前创建transition的id要小的动画。
32行代码timer.c赋值为tick,之后每个动画帧周期调用都是tick函数了。33~39行代码就是在对当前动画的执行在安排一次,并且也只执行一次,这里不是很明白为什么要这么做,对于明白的大牛请在评论中告诉我。41行代码通知调用注册的start事件的回调函数。42~47行代码用来保存用户定义的插值函数,对于插值函数不会在这篇博客中进行讨论,我会在下篇博客中进行详细的阐述。
对于d3_transitionNode函数我们需要讨论就是tick函数,这个函数就是用来不断的执行js动画,如果执行晚了话,将动画参数从lock中删除,如果lock中不存在transition参数,就将ns对应的属性给删除。然后还通知调用注册了end事件的回调函数。
讨论了这么久终于把基础的transition给讲完了,但是还没有结束哟,对应看到这里的朋友,我觉得真的很不容易,毕竟这么多文字,我觉得如果还想看下去,建议还是休息一下。
transition四种状态
transition一共有四种状态,scheduling、start、run和end状态。
scheduling状态就是在调用transition函数创建transition时,或者在调用duration、delay等配置transition函数时,或者调用attr、style等函数来指定最后transition最后状态时的状态,因为js是单线程的,而schedule、start和tick函数都是异步调用的,所以调用上面提到的那些函数都没有问题,是同步的。
start状态就是调用start函数时的状态
run状态就是在不断的调用run函数执行动画时的状态
end状态就是在动画执行结束时的状态
subtransition
对于subtransition我们这里通过transition.each函数简单的讨论一下:
d3_transitionPrototype.each = function(type, listener) { var id = this.id, ns = this.namespace; if (arguments.length < 2) { var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; try { d3_transitionInheritId = id; d3_selection_each(this, function(node, i, j) { d3_transitionInherit = node[ns][id]; type.call(node, node.__data__, i, j); }); } finally { d3_transitionInherit = inherit; d3_transitionInheritId = inheritId; } } else { d3_selection_each(this, function(node) { var transition = node[ns][id]; (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); }); } return this;};
因为each函数有两种用法,第一种用法时用来给元素的动画绑定start,interrupt和end事件的回调函数的,我们会在后面的内容详细介绍的。
第二种用法只传递一个function类型的函数,然后对于transition中的每个元素进行调用,如果在这个回调函数中创建transition的话,我们称这个transition就是subtransition,这段代码对应着4~14行代码,我们可以清楚的了解到d3_transitionInheritId和d3_transitionInherit被指定了相应的值,如果在function类型的参数中创建transition的话,新创建的subtransition是会继承d3_transitionInheritId和d3_transitionInherit。其实比较简单,对不对。
interrupt当前执行的动画
之前我们在d3_transitionNode的start函数中提到过一次,那是一种中断transition的方法,d3还提供了另一种中断transition的方法——selection.interrupt,我们看看是怎么实现的呗:
d3_selectionPrototype.interrupt = function(name) { return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name)));};var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace());function d3_selection_interruptNS(ns) { return function() { var lock, activeId, active; if ((lock = this[ns]) && (active = lock[activeId = lock.active])) { active.timer.c = null; active.timer.t = NaN; if (--lock.count) delete lock[activeId]; else delete this[ns]; lock.active += .5; active.event && active.event.interrupt.call(this, this.__data__, active.index); } };}
实现原理很简单,就是将当前执行的动画timer.c赋值为null,这样在d3_timer调用d3_timer_sweep函数的时候就会将当前执行的transition给删除了。
start、interrupt、end事件
在transition中提供了一些方法来监听每个元素的每个动画的开始,中断和结束事件,主要是通过transition.each来实现,所以我们继续回到each函数:
d3_transitionPrototype.each = function(type, listener) { var id = this.id, ns = this.namespace; if (arguments.length < 2) { var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; try { d3_transitionInheritId = id; d3_selection_each(this, function(node, i, j) { d3_transitionInherit = node[ns][id]; type.call(node, node.__data__, i, j); }); } finally { d3_transitionInherit = inherit; d3_transitionInheritId = inheritId; } } else { d3_selection_each(this, function(node) { var transition = node[ns][id]; (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); }); } return this;};
这次我们只需要关注16~19行代码,可以比较比较容易的了解到,首先获得每个元素的transition配置数据,然后调用d3.dispatch将绑定的事件对象保存在transition.event中。当对应时间发生的时候就会调用对象的函数。为了进一步的了解是如何实现的,我们需要看看d3.dispatch函数的实现细节:
d3.dispatch = function() { var dispatch = new d3_dispatch(), i = -1, n = arguments.length; while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); return dispatch;};function d3_dispatch() {}d3_dispatch.prototype.on = function(type, listener) { var i = type.indexOf("."), name = ""; if (i >= 0) { name = type.slice(i + 1); type = type.slice(0, i); } if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); if (arguments.length === 2) { if (listener == null) for (type in this) { if (this.hasOwnProperty(type)) this[type].on(name, null); } return this; }};function d3_dispatch_event(dispatch) { var listeners = [], listenerByName = new d3_Map(); function event() { var z = listeners, i = -1, n = z.length, l; while (++i < n) if (l = z[i].on) l.apply(this, arguments); return dispatch; } event.on = function(name, listener) { var l = listenerByName.get(name), i; if (arguments.length < 2) return l && l.on; if (l) { l.on = null; listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); listenerByName.remove(name); } if (listener) listeners.push(listenerByName.set(name, { on: listener })); return dispatch; }; return event;}
d3.dispatch 、3_dispatch和d3_dispatch.prototype.on都比较简单,所以这里就不解释了,我们主要来分析一下d3_dispatch_event函数:
function d3_dispatch_event(dispatch) { var listeners = [], listenerByName = new d3_Map(); function event() { var z = listeners, i = -1, n = z.length, l; while (++i < n) if (l = z[i].on) l.apply(this, arguments); return dispatch; } event.on = function(name, listener) { var l = listenerByName.get(name), i; if (arguments.length < 2) return l && l.on; if (l) { l.on = null; listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); listenerByName.remove(name); } if (listener) listeners.push(listenerByName.set(name, { on: listener })); return dispatch; }; return event;}
这个函数的作用其实使用返回一个函数event,这个event函数和三个start、interrupt、end事件中的其中一个绑定,通过event.on可以用来绑定对应的回调函数。当事件发生的时候就可以通过调用event函数来执行那些绑定的回调函数。
总结
花了一天半的时间分析transition源码,感觉要不d3的data join要难,可能是因为我对api不熟悉的原因吧。这次分析中其实有一个很重要的东西没有分析,就是在执行transition执行中是如何进行插值的,对于插值我会在下篇博客中分析。
还是老话了,每天进步一点,希望毕业的时候能找份好工作,加油!
- d3.js学习笔记(2)—— Transition
- D3.js 数据可视化学习笔记——Hello D3!
- D3.js学习笔记十二:D3.js构图(d3.layout)——力学(Force)图
- d3.js 可视化学习笔记(一)——柱形图
- D3.js 学习笔记
- d3.js 学习笔记
- d3.js学习笔记
- 精通D3.js学习笔记(2)比例尺和坐标
- d3.js学习笔记(一)
- d3.js学习笔记(二) 柱形图
- d3.js学习笔记(1)
- D3.js学习笔记(一)——DOM上的数据绑定
- D3.js学习笔记(二)——使用绑定在DOM上的数据
- D3.js学习笔记(三)——创建基于数据的SVG元素
- D3.js学习笔记(一)——DOM上的数据绑定
- D3.js学习笔记(二)——使用绑定在DOM上的数据
- D3.js学习笔记(三)——创建基于数据的SVG元素
- d3.js 可视化学习笔记(二)——svg导出png保存
- UVa 489
- python Unicode编程
- linux环境下关闭后台进程
- TCP/IP
- 数据库中的日期
- d3.js学习笔记(2)—— Transition
- Android中给App设置NoActionBar/FullScreen
- 第四章笔记
- 2016sdau课程练习专题三 1017
- scala学习之update
- Tensorflow的MNIST进阶教程CNN网络参数理解
- Line Disciplines
- 堆和栈的区别
- 首页-底部Tab导航(菜单栏)的实现:FragmentTabHost+ViewPager+Fragment