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执行中是如何进行插值的,对于插值我会在下篇博客中分析。

还是老话了,每天进步一点,希望毕业的时候能找份好工作,加油!

1 0
原创粉丝点击