d3.js学习笔记—— Transition
Posted wlbreath
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了d3.js学习笔记—— Transition相关的知识,希望对你有一定的参考价值。
今天是纪录学习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学习笔记—— Transition的主要内容,如果未能解决你的问题,请参考以下文章