三、doAnimation内部的Animation()方法
作用:
$().animate()核心方法

源码:

  //animate()核心方法
  //源码7844行
  //elem:目标元素

  //this:目标元素
  //{'width': '500'}
  // optall={
  //   complete:function(){jQuery.dequeue()},
  //   old:false,
  //   duration: 400,
  //   easing: undefined,
  //   queue:"fx",
  // }

  function Animation( elem, properties, options ) {
    var result,
      stopped,
      index = 0,
      //1
      length = Animation.prefilters.length,
      //{
      // always:function(){},
      // catch:function(){},
      // done:function(){},
      // xxx
      // }

      //初始化deferred对象
      //deferred.always()表示不管成功还是失败,最终都会运行内部设置的代码
      deferred = jQuery.Deferred().always( function() {

        // Don't match elem in the :animated selector
        delete tick.elem;
      } ),
      
      tick = function() {
        if ( stopped ) {
          return false;
        }
        var currentTime = fxNow || createFxNow(),
          remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),

          // Support: Android 2.3 only
          // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
          temp = remaining / animation.duration || 0,
          percent = 1 - temp,
          index = 0,
          length = animation.tweens.length;

        for ( ; index < length; index++ ) {
          animation.tweens[ index ].run( percent );
        }

        deferred.notifyWith( elem, [ animation, percent, remaining ] );

        // If there's more to do, yield
        if ( percent < 1 && length ) {
          return remaining;
        }

        // If this was an empty animation, synthesize a final progress notification
        if ( !length ) {
          deferred.notifyWith( elem, [ animation, 1, 0 ] );
        }

        // Resolve the animation and report its conclusion
        deferred.resolveWith( elem, [ animation ] );
        return false;
      },
      //==========tick end==========
      //让animation带有promise的属性,并在其中添加动画的属性和方法
      animation = deferred.promise( {
        elem: elem,
        props: jQuery.extend( {}, properties ),
        opts: jQuery.extend( true, {
          specialEasing: {},
          easing: jQuery.easing._default
        }, options ),
        originalProperties: properties,
        originalOptions: options,
        startTime: fxNow || createFxNow(),
        duration: options.duration,
        tweens: [],
        //500,'width',animation
        createTween: function( prop, end ) {
          var tween = jQuery.Tween( elem, animation.opts, prop, end,
            animation.opts.specialEasing[ prop ] || animation.opts.easing );
          animation.tweens.push( tween );
          // {
          //   easing: "swing"
          //   elem: div#A
          //   end: 500
          //   now: 500
          //   options: {specialEasing: {…}, easing: "swing", complete: ƒ, duration: 400, queue: "fx", …}
          //   pos: 1
          //   prop: "width"
          //   start: 100
          //   unit: "px"
          // }
          return tween;
        },
        stop: function( gotoEnd ) {
          var index = 0,

            // If we are going to the end, we want to run all the tweens
            // otherwise we skip this part
            length = gotoEnd ? animation.tweens.length : 0;
          if ( stopped ) {
            return this;
          }
          stopped = true;
          for ( ; index < length; index++ ) {
            animation.tweens[ index ].run( 1 );
          }

          // Resolve when we played the last frame; otherwise, reject
          if ( gotoEnd ) {
            deferred.notifyWith( elem, [ animation, 1, 0 ] );
            deferred.resolveWith( elem, [ animation, gotoEnd ] );
          } else {
            deferred.rejectWith( elem, [ animation, gotoEnd ] );
          }
          return this;
        }
      } ),
      //===========animation end===============
      props = animation.props;
    //{width:500},undefined
    propFilter( props, animation.opts.specialEasing );

    for ( ; index < length; index++ ) {
      result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts );
      if ( result ) {
        if ( isFunction( result.stop ) ) {
          jQuery._queueHooks( animation.elem, animation.opts.queue ).stop =
            result.stop.bind( result );
        }
        return result;
      }
    }
    /*运行动画*/
    // createTween(500,'width',animation)
    jQuery.map( props, createTween, animation );

    if ( isFunction( animation.opts.start ) ) {
      animation.opts.start.call( elem, animation );
    }

    // Attach callbacks from options
    animation
      .progress( animation.opts.progress )
      .done( animation.opts.done, animation.opts.complete )
      .fail( animation.opts.fail )
      .always( animation.opts.always );

    jQuery.fx.timer(
      //让tick方法继承elem、anim和queue属性
      jQuery.extend( tick, {
        elem: elem,
        anim: animation,
        queue: animation.opts.queue
      } )
    );

    return animation;
  }

解析:

(1)Animation.prefilters
源码:

jQuery.Animation = jQuery.extend( Animation, {
    //源码8175行
    //defaultPrefilter是一个function
    prefilters: [ defaultPrefilter ],
})

所以Animation.prefilters=1defaultPrefilter的源码暂不解析

(2)关于jQuery.Deferred()的解释,请参考:jQuery中的Deferred详解和使用

(3)jQuery.map(elems, callback, arg)
作用:
根据elems数量,循环运行callback( elems[ i ], i, arg )

源码:

jQuery.extend( {
    // arg is for internal usage only
    //源码524行
    //props, createTween, animation
    map: function( elems, callback, arg ) {
      var length, value,
        i = 0,
        ret = [];

      // Go through the array, translating each of the items to their new values
      //如果elems是类数组的话
      if ( isArrayLike( elems ) ) {
        length = elems.length;
        for ( ; i < length; i++ ) {
          value = callback( elems[ i ], i, arg );

          if ( value != null ) {
            ret.push( value );
          }
        }

        // Go through every key on the object,
      } else {
        //走这边
        for ( i in elems ) {
          //500 width animation
          /*执行动画*/
          value = callback( elems[ i ], i, arg );

          if ( value != null ) {
            ret.push( value );
          }
        }
      }
      console.log(ret,'ret555')
      // Flatten any nested arrays
      // 展平任何嵌套数组
      return concat.apply( [], ret );
    },

})

解析:
根据例子的话,就是:

createTween(500,'width',animation)
createTween(300,'width',animation)
createTween(1000,'width',animation)

(4)jQuery内部函数createTween(value, prop, animation)
作用:
animation调用Animation.tweeners[ "*" ]中的方法

源码:

  //源码7752行
  //创建动画对象
  // createTween(500,'width',animation)
  function createTween( value, prop, animation ) {
    var tween,
      //[ function( prop, value ) {
      //         var tween = this.createTween( prop, value );
      //         console.log('vvvv','aaa8083')
      //         adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
      //         return tween;
      //       } ]
      collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ),
      index = 0,
      //1
      length = collection.length;
    for ( ; index < length; index++ ) {
      //prop:width
      //value:500
      //运行collection[ index ],this绑定animation
      if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) {

        // We're done with this property
        return tween;
      }
    }
  }

(5)Animation.tweeners[ "*" ]
作用:
animation调用Animation.tweeners[ "*" ]中的方法

  jQuery.Animation = jQuery.extend( Animation, {
    //源码8152行
    tweeners: {
      //prop:width
      //value:500
      "*": [ function( prop, value ) {
        //animation.createTween
        var tween = this.createTween( prop, value );
        adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween );
        return tween;
      } ]
    },

})

解析:
返回经过animation. createTween('width',500)处理和adjustCSS()处理的变量tween

① animation. createTween('width',500)
animationAnimation()方法中封装的一个对象(对象keyvaluefunction

作用:
根据开发者传入的属性,将其转化为一个对象,对象内部的属性时执行动画所需要的属性。

源码:

animation = deferred.promise( {
   //500,'width',animation
   createTween: function( prop, end ) {
      var tween = jQuery.Tween( elem, animation.opts, prop, end,
          animation.opts.specialEasing[ prop ] || animation.opts.easing );
          animation.tweens.push( tween );
          // {
          //   easing: "swing"
          //   elem: div#A
          //   end: 500
          //   now: 500
          //   options: {specialEasing: {…}, easing: "swing", complete: ƒ, duration: 400, queue: "fx", …}
          //   pos: 1
          //   prop: "width"
          //   start: 100
          //   unit: "px"
          // }
          return tween;
      },
})

解析:
调用jQuery.Tween获得tween对象,并把tween对象放进animation.tweens数组中

② 简单看下jQuery.Tween源码:

  //源码7568行
  function Tween( elem, options, prop, end, easing ) {
    //width 500 swing
    //width 300 swing
    //width 1000 swing
    return new Tween.prototype.init( elem, options, prop, end, easing );
  }
  jQuery.Tween = Tween;

  Tween.prototype = {
    constructor: Tween,
    init: function( elem, options, prop, end, easing, unit ) {
      this.elem = elem;
      this.prop = prop;
      this.easing = easing || jQuery.easing._default;
      this.options = options;
      this.start = this.now = this.cur();
      this.end = end;
      this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
    },
    cur: function() {},
    run: function( percent ) {},
  };

  Tween.prototype.init.prototype = Tween.prototype;

执行jQuery.Tween方法,就是new一个对象,就是执行jQuery.Tween.init()方法,根据{width:500}生成的动画对象如下:

{
  easing: "swing"
  elem: div#A
  end: 500
  now: 500
  options: {specialEasing: {…}, easing: "swing", complete: ƒ, duration: 400, queue: "fx", …}
   pos: 1
   prop: "width"
   start: 100
   unit: "px"
}

③ 关于adjustCSS的解析,请看:jQuery源码解析(4)—— css样式、定位属性

Animation.tweeners[ "*" ]方法最终返回的tween如下:

{
  easing: "swing"
  elem: div#A
  end: 500
  now: 500
  options: {specialEasing: {…}, easing: "swing", complete: ƒ, duration: 400, queue: "fx", …}
   pos: 1
   prop: "width"
   start: 100
   unit: "px"
}

综上,jQuery.map()最终作用就是将$().animate()中的参数转化为动画对象,并pushanimation.tweens数组中

(6)jQuery.fx.timer()
作用:
依次执行timer

源码:

  //源码8504行
  //单个动画内部执行
  jQuery.fx.timer = function( timer ) {
    //将Animation.tick()依次放进jQuery.timers数组中
    jQuery.timers.push( timer );
    //每push进一个,就运行一个
    jQuery.fx.start();
  };

jQuery.timers是一个数组:

//源码8431行
  jQuery.timers = [];

(7)jQuery.fx.start()
作用:
在动画运行前,加锁,并运行动画

源码:

  //源码8514行
  //加锁,运行动画
  jQuery.fx.start = function() {
    if ( inProgress ) {
      return;
    }
    //动画开始即为运行中,加上锁
    inProgress = true;
    //运行
    schedule();
  };

注意:inProgress 锁是控制整个动画流程的锁,而不是单个动画队列的锁

(8)schedule()
作用:
如果动画已经开始(inProgress=true),那么就不断执行jQuery.fx.tick()方法(动画渲染)

源码:

  //源码7694行
  //如果动画已经开始,那么就不断执行jQuery.fx.tick()方法(动画渲染)
  function schedule() {
    //inProgress是判断整个动画流程是否结束的标志
    //当inProgress=null时,整个动画结束
    if ( inProgress ) {
      //走这边
      if ( document.hidden === false && window.requestAnimationFrame ) {
        //使用requestAnimationFrame来完成动画
        //递归
        window.requestAnimationFrame( schedule );
      } else {
        //13代表动画每秒运行的帧数,可以保证浏览器能完成动画
       //jQuery.fx.interval = 13;
        window.setTimeout( schedule, jQuery.fx.interval );
      }
      /*执行动画*/
      jQuery.fx.tick();
    }
  }

(9)jQuery.fx.tick()
作用:
运行Animation.tick()并安全地移除它

源码:

  //源码8483行
  //运行Animation.tick()并安全地移除它
  jQuery.fx.tick = function() {
    var timer,
      i = 0,
      timers = jQuery.timers;

    fxNow = Date.now();
    //这里的timers,就是Animation.tick()的集合
    for ( ; i < timers.length; i++ ) {
      timer = timers[ i ];

      // Run the timer and safely remove it when done (allowing for external removal)
      //运行Animation.tick()并安全地移除它
      if ( !timer() && timers[ i ] === timer ) {
        timers.splice( i--, 1 );
      }
    }
    //inProgress=null,停止动画
    if ( !timers.length ) {
      jQuery.fx.stop();
    }
    fxNow = undefined;
  };

  //源码8474行
  //结束整个动画流程
  jQuery.fx.stop = function() {
    inProgress = null;
  };

(10)Animation.tick()
作用:
根据动画的参数来执行动画

源码:

function Animation( elem, properties, options ) {
//根据动画的参数来执行动画
      tick = function() {
        if ( stopped ) {
          return false;
        }
        //当前时间的时间戳
        var currentTime = fxNow || createFxNow(),
          //动画时长默认400ms
          //开始时间+动画时长-当前时间
          //在每次调用requestAnimationFrame后,记录下剩下的的时间在总时间(duration)中的位置
          remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),

          // Support: Android 2.3 only
          // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497)
          //剩下的时间占总时长的占比
          temp = remaining / animation.duration || 0,
          //当前时间占总时长的占比
          percent = 1 - temp,
          index = 0,
          length = animation.tweens.length;

        for ( ; index < length; index++ ) {
          //根据传入的动画参数和当前进程的百分比来运行动画
          animation.tweens[ index ].run( percent );
        }

        deferred.notifyWith( elem, [ animation, percent, remaining ] );

        // If there's more to do, yield
        if ( percent < 1 && length ) {
          return remaining;
        }

        // If this was an empty animation, synthesize a final progress notification
        if ( !length ) {
          deferred.notifyWith( elem, [ animation, 1, 0 ] );
        }

        // Resolve the animation and report its conclusion
        deferred.resolveWith( elem, [ animation ] );
        return false;
      },

}

解析:
通过动画持续时间duration、动画开始时间animation.startTime和每次调用requestAnimationFrame后动画结束时间currentTime,计算出此帧在整个动画流程中的占比,从而较为准确绘制动画

(11)Tween.run()
作用:
绘制动画帧

源码:

  Tween.prototype = {
      run: function( percent ) {
      // {
      //   easing: "swing"
      //   elem: div#A
      //   end: 500
      //   now: 105.52601592046467
      //   options: {specialEasing: {…}, easing: "swing", complete: ƒ, duration: 400, queue: "fx", …}
      //   pos: 1
      //   prop: "width"
      //   start: 100
      //   unit: "px"
      // }
      var eased,
        //undefiend
        hooks = Tween.propHooks[ this.prop ];
      //400
      if ( this.options.duration ) {
        //swing,两边慢中间快
        //动画效果
        this.pos = eased = jQuery.easing[ this.easing ](
          percent, this.options.duration * percent, 0, 1, this.options.duration
        );
      } else {
        this.pos = eased = percent;
      }
      //width的宽度
      this.now = ( this.end - this.start ) * eased + this.start;

      if ( this.options.step ) {
        this.options.step.call( this.elem, this.now, this );
      }

      if ( hooks && hooks.set ) {
        hooks.set( this );
      } else {
        //走这边
        //执行style变化
        Tween.propHooks._default.set( this );
      }
      return this;
    },

  }

解析:
一个是动画效果swing的处理:jQuery.easing[ this.easing ](percent, this.options.duration * percent, 0, 1, this.options.duration);

另一个就是关键style变化了:Tween.propHooks._default.set( this )

(12)Tween.propHooks._default.set()
作用:
执行style变化

源码:

  Tween.propHooks = {
    _default: {
      //源码7661行
      set: function( tween ) {  
        // Use step hook for back compat.
        // Use cssHook if its there.
        // Use .style if available and use plain properties where available.
        //undefined
        if ( jQuery.fx.step[ tween.prop ] ) {
          jQuery.fx.step[ tween.prop ]( tween );
        } else if ( tween.elem.nodeType === 1 &&
          ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null ||
            jQuery.cssHooks[ tween.prop ] ) ) {
          //走这边
          //#A,width,100px(103px,134px,xxx)
          jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
        } else {
          tween.elem[ tween.prop ] = tween.now;
        }
      },

  }
}

解析:
tween.now,是每次requestAnimationFrame要变化的width的值,tween.unitpx,所以这段代码最终执行的是jQuery.style( 目标元素, 要变化的style属性, 要变化的值 )

(13)jQuery.style()
作用:
设置 DOM 节点的 style 属性

简略的源码:

    // Get and set the style property on a DOM Node
    //源码7279行
    style: function( elem, name, value, extra ) {
         elem.style[ name ] = value
    }

综上,Animation() 有两大作用:
(1)将传入的动画对象处理成jQuery的动画对象。
(2)根据duration的间隔,利用requestAnimationFrame循环执行style,从而达到渲染动画的目的。

最后,附上 doAnimation() 的流程图,建议配合整个$().animate()的流程图(二、的最后一个图)一起看:

点击放大看

下篇将会模拟实现$().animate() 方法,敬请期待!


(完)


小进进
113 声望9 粉丝