7.jQuery源码动画系统的设计

前言

本篇文章以jquery v3.5.1版本为例,跟踪一轮动画的运行过程,窥探其动画系统的设计架构.下面是html测试代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="./lib/jquery.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .main {
        width: 1200px;
        margin: 0 auto;
        border: 1px solid red;
        height: 800px;
        position: relative;
      }
      .top {
        height: 100px;
        width: 100px;
        border: 1px solid #eee;
        background-color: #eee;
        opacity: 1;
      }
      .bottom {
        border: 1px solid #000;
        position: absolute;
        top: 100px;
        bottom: 0;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div class="main">
      <div class="top">
        hello world
      </div>
      <div class="bottom"></div>
    </div>
  </body>
  <script>
    $('.top').animate(
      {
        height: '300px',
        opacity: 1,
      },
      2000
    );
    $('.top').animate(
      {
        width: '600px',
      },
      2000
    );
  </script>
</html>

运行结果:

   

                               

运行跟踪

1.

                                         

  • 这是进入的第一个函数.prop是我们在html页面传入的对象{height:"300px",opacity:1},speed等于5000,后面的两个参数为空
  • 这个函数的主要作用是定义了一个doAnimation函数,随后运行this.queue进入队列函数.optall.queue为"fx",队列里面用到.

 2.

                                            

  • queue是队列函数.type为"fx",data为上面传递过来的doAnimation.
  • 线程直接进入到this.each定义的函数中.jQuery.queue这个函数传入dom对象,"fx"和doAnimation三个参数.它会在dom对象上挂载一个新的对象,key值为"fxqueue",value为[doAnimation].并返回该对象赋值给queue.
  • 4786行代码.jQuery.queue就是给挂载在dom对象上面的一个缓存数组(也就是上图的queue)添加动画函数的.运行一次就会往数组push一个动画函数,这样就可以给dom元素绑定多个动画函数并设置了先后执行的次序.
  • 接下来会运行jQuery.dequeue.从上面的gif动图可以看出来我们给$(".top")加了两个动画,它会在第一次执行动画完成后才会执行第二个动画.那在代码中如何实现这种先后执行的次序的呢?关键点在于4791行代码.queue[0] !== 'inprogress'.当执行第一个动画的过程中时,queue的第0个元素被改成了字符串'inprogress',也就是说在第一个动画没有执行完成之前,jQuery.dequeue是不会被执行的.接下来我们看下jQuery.dequeue里面是怎么修改queue数组的元素并把它的第一个元素置成了"inprogress".

3.

                                      

  • var queue = jQuery.queue(elem, type).把dom对象上挂载的队列取出来赋值给queue.queue的值为[doAnimation]
  • 首次运行,fn等于函数doAnimation.接下里会执行queue.unshift("inprogress").queue的值为["inprogress"]
  • 随后会运行fn并给它传入next函数.运行fn其实就是运行doAnimation.饶了这么一大圈最终的目的就是运行我们在第一步定义的那个doAnimation函数.
  • 这个dequeue函数到底做了什么事情呢?它首先取出挂载dom上面的一个数组对象,最开始里面只装了一个元素doAnimation.它每次执行都会通过shift取出该数组的第一个元素赋值给fn,随后呢又给该数组的头部塞进去一个字符串"inprogress",接下来就去执行fn.塞这个"inprogress"的作用就是说我当前在执行一个动画,如果有其他动画函数得到线程的使用权需要执行时它们会判端队列的第一个元素是不是"inprogress",如果是的话它们就放弃执行.等到当前这个动画终于执行完毕了会再一次调用jQuery.dequeue函数看看还有没有处于等待状态的其他动画需要执行.那么这一次执行它仍然会先取出数组的第一个元素也就是字符串"inprogress"赋值给fn,fn是个字符串肯定执行不了,所以再一次运行fn = queue.shift().这样fn就成功得到了下一个动画函数,只要fn有值就执行fn开始运行第二个动画了.总结分析而言,挂载dom上面的数组对象里面盛放了该dom对象需要做的一堆动画函数,每执行一次dequeue函数就会从这个数组的头部获取一个函数来执行,再运行一次dequeue函数就开始调用第二个动画函数,依次类推.换句话将,你想要让dom元素运行动画函数只能通过调用dequeue函数来做,调用一次就运行一个.比如在恰当的时机第一个动画已经完成了,再运行一次dequeue函数就可以执行第二个动画了.在其他时候想运行dequeue函数它的代码前一般都会加个判端条件,判端数组的第一个元素是不是等于"inprogress",如果有就不让执行dequeue函数.

4.

                                           

  • 好了我们成功地绕了一大圈又绕回到了第一个函数.现在开始执行缓存队列queue的第一个动画函数也就是上图中标红的doAnimation函数.
  • prop是我们在html页面第一个动画函数中定义的最终的状态 {height: "300px"opacity: 1}.而optall是一个对象,它里面有三个属性{duration: 2000,queue: "fx",easing: undefined}.分别记录了动画执行时间,绑定在dom上面的缓存动画函数数组对应的key值以及动画过渡函数easing.
  • Animation函数里面编写的便是执行动画的真正逻辑.它会根据用户定义的最终的样式状态和optall让dom元素动起来.

5.

function Animation( elem, properties, options ) {
	var result,
		stopped,
		index = 0,
		length = Animation.prefilters.length,
		deferred = jQuery.Deferred().always( function() {

			// Don't match elem in the :animated selector
			delete tick.elem;
		} ),
		tick = function() {
			...
		},
		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: [],
			createTween: function( prop, end ) {
				var tween = jQuery.Tween( elem, animation.opts, prop, end,
						animation.opts.specialEasing[ prop ] || animation.opts.easing );
				animation.tweens.push( tween );
				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;
			}
		} ),
		props = animation.props;

	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;
		}
	}

	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(
		jQuery.extend( tick, {
			elem: elem,
			anim: animation,
			queue: animation.opts.queue
		} )
	);

	return animation;
}
  • 这个Animation函数接受三个参数.elem是要做动画的dom元素,properties是用户填写样式,第一次触发它的值为{height: "300px", opacity: 1}.options里面有三个属性值{duration: 2000,queue: "fx",easing: undefined}.
  • Animation创建一个tick函数和animation变量.tick函数的功能后面再说,而animation是通过deferred.promise创建的一个延时对象(延时对象在之前的章节中已经介绍过).
  • 创建animation这个变量最核心的一个作用的就是给animation对象添加一个属性tweens.通过下面这句代码 jQuery.map(props, createTween, animation); 来完成这个功能.我们看下添加完tweens的值是什么样的  
  •        
  • animation这个对象多了一个tweens属性,对应的是一个数组.数组中的每一个对象对应的是上述properties里面的每一个样式对应的状态.比如用户定义的height:"300px".最后被解析成了起始状态start:100.终点状态end:300.创建时当前状态now等于起始状态start.elem指的是做动画的dom元素.easing是动画函数.prop是动画的属性.值得一提的是start的值在底层是通过getComputedStyle这个API得到的.
  • 综上所述tweens把要改变的属性全部解析成了做动画所需要的所有数据.比如说每一帧都遍历这个tweens数组,每一次只要计算出now的值,然后呢将该值赋予该dom元素的样式上,这样就能让dom元素动起来了.那如何随着时间流逝计算每一帧的now值就成了接下来重要的任务.

6.

                                                       

                                                    

                                                        

  • 在上述Animation函数的最后部分执行了jQuery.fx.timer.将tick函数作为参数传递了过去.经过了一系列的处理随后进入了schedule函数.
  • schedule函数里面我可以看到它是通过window.requestAnimationFrame来实现递归调用jQuery.fx.tick这个函数.只要inProgress不为false,那么每一帧都会去执行一次jQuery.fx.tick.
  • jQuery.fx.tick这个函数又是干什么的呢?jQuery.timers是一个数组里面盛放的是我们最开始传递过去的tick函数.它取出tick函数并赋值给timer执行.根据的timer()的返回值来决定要不要执行jQuery.fx.stop();   jQuery.fx.stop()一旦执行inProgress就会被设置为null.
  • 从这里我们可以看出来tick函数返回结果直接决定了inProgress的值,而inProgress又决定是否要递归调用jQuery.fx.tick.
  • 综上所述.这一块就是jquery的一个定时调用的模块.你传递一个函数tick给它,它每一帧就会去执行tick函数,根据tick函数的返回值来判断是否停止循环调用.

7.

                                           

  • 这个tick函数就是定时模块每一帧都会去调用的函数体.它首先定义了一个当前的时间戳currentTime,随后计算出当前时刻动画应该完成的百分比percent.根据第五步的描述可知animation.tweens里面可以包含了每个属性的start和end值.那如果使用(end-start)*percent不就得出了此时该属性的now值了吗.animation.tweens[index].run这个方法干的就是这个事情.
  •                                     
  • 得出了now值,在7449行通过调用set方法给dom元素的style上的相应属性附上now的值,如此改变了dom的样式
  • 再加上上面定时模块的调用.每一帧都执行一次tick函数,每执行一次tick函数就计算一次now值修改的dom样式,这样便形成了动画效果.
  • 从第6步可知tick函数的返回值决定了帧调用何时停止.7832行代码如果percent<1那么它会返回一个值.percent大于或等于1时说明动画结束了,于是返回flase.
  • 第7842行deferred.resolveWith(elem, [animation]);这句代码是在动画执行完毕之后才会调用.deferred是一个延时对象,当它调用resolveWith方法时,它事先绑定的done函数就会被触发.我们来看下done函数

8.

                                 

                                  

  • 7927行定义了done函数,最终绑定的是animation.opts.complete函数
  • opt.complete函数里面最终调用的是前面提到的jQuery.dequeue.这样就保证了一个动画函数调用完毕了再开始调用第二个动画函数.

 

总结

  • 动画系统的运行架构从用户定义$.animate函数开始运行起便创建一个doAnimation函数,并将它放到一个数组中绑定到dom对象上.它之所以这样做为了方便管理多组动画.
  • 随后开始按从前到后的顺序依次运行数组中的doAnimation函数.doAnimation里面的核心功能就是通过用户定义$.animate函数时的动画持续时间duration来计算出当前时间占duration的百分比percent,并结合用户传入的那些终点状态的样式值计算出当前时刻应该对应的样式值,随后更新到dom元素上.这是每一帧运行tick函数所做的事.
  • tick函数的返回值可以成为递归调用tick函数的循环条件.每一帧运行一次tick函数便改变一下dom样式,随着帧数的连续调用就形成了动画效果.

猜你喜欢

转载自blog.csdn.net/brokenkay/article/details/107233953