d3.js学习笔记—— Transition

Posted wlbreath

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了d3.js学习笔记—— Transition相关的知识,希望对你有一定的参考价值。

今天是纪录学习d3的第二篇博客,主要还是从源码的角度来分析d3的transition是如何实现的。这篇文章可能比较长,不过如果能够读完的话,我觉得肯定会对d3有更加深刻的认识。

学习资料

和上一篇一样首先列一下学习的资料,这些都能够在d3的github上能够找到

简单的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的主要内容,如果未能解决你的问题,请参考以下文章

D3.js 动画 过渡效果 (V3版本)

D3.js学习

d3.js学习笔记

d3.js学习笔记

精通D3.js学习笔记基础的函数

D3.js:动态效果