jq源码 | 2020-12-31 15:24:49 371次 1次
动画入口
jq 动画这里采用队列的形式进行动画的回调操作,比如进行一个动画 $(xx).animate().animate() 的连续调用,会等待第一个执行完毕再执行第二个动画,看下这里的设计:
入口:
jQuery.fn.extend( { animate: function( prop, speed, easing, callback ) { var empty = jQuery.isEmptyObject( prop ), optall = jQuery.speed( speed, easing, callback ), doAnimation = function() { // 这个 方法 下面再看 var anim = Animation( this, jQuery.extend( {}, prop ), optall ); if ( empty || dataPriv.get( this, "finish" ) ) { anim.stop( true ); } }; doAnimation.finish = doAnimation; return empty || optall.queue === false ? this.each( doAnimation ) : // 通过 queue 执行存储队列 this.queue( optall.queue, doAnimation ); } }
queue 的设计分别挂载到了 $.fn 原型和 $ 自身静态方法,静态方法才是真正的进行数据结构的存储,原型上只是负责总体调度执行:
jQuery.fn.extend( { queue: function( type, data ) { ... this.each( function() { // 取出一个任务 var queue = jQuery.queue( this, type, data ); ... if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { // 执行 jQuery.dequeue( this, type ); } } ); }, }
如果有两个 animate 队列,此时的 queue 数据结构如下:
['inprogress', doAnimation]
说明第一个任务正在进行中,第二个任务在排队等着。第一个调用 animate 方法时 queue 中只有一个任务,那么会进入 jQuery.dequeue 去执行任务,因为是连续调用,则第二个 animate 进入时,队列中存在一个进行中和刚传入的任务,第一个任务执行完毕再取出第二个执行,保证了动画的执行是连续的串行执行。
队列
上面提到了 queue 和 dequeue,队列是一种先进先出的数据结构,大概了解了动画的一个执行流程控制,这里看下队列的内部设计:
jQuery.extend( { queue: function( elem, type, data ) { var queue; if ( elem ) { // 动画标识 type = ( type || "fx" ) + "queue"; // 缓存中取这个队列 queue = dataPriv.get( elem, type ); // 这里判断 data 是因为 dequeue 执行时先从缓存中取数据 if ( data ) { if ( !queue || Array.isArray( data ) ) { // 创建 queue 并存入缓存 queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); } else { // animate 连续调用时 就追加这个队列了 queue.push( data ); } } return queue || []; } }, dequeue: function( elem, type ) { type = type || "fx"; // 取出队列 var queue = jQuery.queue( elem, type ), startLength = queue.length, // 取出先进入的任务 fn = queue.shift(), hooks = jQuery._queueHooks( elem, type ), // next 提供自身方法的再次调用 // 动画中没有用到这个 但是这个 queue方法可以单独使用,自己处理一些逻辑 next = function() { jQuery.dequeue( elem, type ); }; // 上个任务执行完毕 再取出下一个任务 if ( fn === "inprogress" ) { fn = queue.shift(); startLength--; } if ( fn ) { // 下个任务还存在将状态变为 进行中 if ( type === "fx" ) { queue.unshift( "inprogress" ); } // 触发 doAnimation delete hooks.stop; fn.call( elem, next, hooks ); } ... }, }
Animation
上面两个过程只是负责动画的队列执行,动画的核心逻辑在 Animation 方法中。这个方法中做的事情很多,但是核心点就是
1. 如何开启动画
2. 如何计算动画
3. 动画结束后如何开启下一任务
4. 动画如何停止
另外还要考虑一些不能进行动画属性的过滤。
在对这个方法拆解分析之前先看一个简单的动画算法:
const createTime = function(){ return (+new Date) } let startLeft = 500, //元素初始化位置 endLeft = 50, //元素终点位置 duration = 2000, //动画运行时间 startTime = createTime(); //动画开始时间 function tick(){ // 每次变化的时间 let remaining = Math.max(0, startTime + duration - createTime()), temp = remaining / duration || 0, percent = 1 - temp, //每次移动的 left 距离 最终为endLeft leftPos = (endLeft- startLeft) * percent +startLeft; } //开始执行动画 setInterval(tick, 16);
Animation 中都是基于这个核心的算法进行拓展,基于 promise 的事件机制进行控制流程,封装出 tween 方法进行元素位置的动画控制(leftPos 的计算过程)。
开启动画
Animation 方法中最下面开启一个动画执行,通过 jQuery.fx 一系列指令:
// 定义 jQuery.fx = Tween.prototype.init; function Animation( elem, properties, options ) { ... jQuery.fx.timer( jQuery.extend( tick, { elem: elem, anim: animation, queue: animation.opts.queue } ) ); }
jQuery.timers = [] jQuery.fx.timer = function( timer ) { jQuery.timers.push( timer ); jQuery.fx.start(); };
上面的 jQuery.timers 存储一个 queue 中的某一任务,timers 中的某一项会被 requestAnimationFrame 一直执行 直到达到动画目的地。
start 中开始一个调度 schedule(定时器):
function schedule() { if ( inProgress ) { if ( document.hidden === false && window.requestAnimationFrame ) { window.requestAnimationFrame( schedule ); } else { window.setTimeout( schedule, jQuery.fx.interval ); } jQuery.fx.tick(); } }
jQuery.fx.tick = function() { var timer, i = 0, timers = jQuery.timers; ... // tick方法中会计算运动比 未到达100%的时候 timers 队列会一直存在 被schedule调度执行 for ( ; i < timers.length; i++ ) { timer = timers[ i ]; // 执行 Animation 中 Query.extend( tick, ...) 主要是tick if ( !timer() && timers[ i ] === timer ) { timers.splice( i--, 1 ); } } // 停止 if ( !timers.length ) { jQuery.fx.stop(); } ... };
tick 方法会被一直调用,直到动画进度到达 100% ,下面看下动画的计算。
动画计算
计算这里知道了一个核心的计算方式,jq 中的做法将这个过程进行了拆分,依赖 jq 自己提供的 promise 异步机制:
Animation 中 animation 用来组装动画需要的一些数据结构,存入 animation.tweens 交给 tick 使用;
Animation 中 tick 用来计算 运动比例 ,再通过 Tween 进行 dom 的具体运动,根据状态进行 promise 的状态触发;
function Animation( elem, properties, options ) { ... // 用来计算 运动比例 具体的执行交给 Tween 这个方法 tick = function() { ... // 这个过程就是上面说的核心计算发放 var currentTime = fxNow || createFxNow(), remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), percent = 1 - ( remaining / animation.duration || 0 ), index = 0, length = animation.tweens.length; // 去tween中执行 dom 移动 for ( ; index < length; index++ ) { animation.tweens[ index ].run( percent ); } // 动画进行中 deferred.notifyWith( elem, [ animation, percent, remaining ] ); // 动画没有完成,返回一个时间或者超时也要完成 if ( percent < 1 && length ) { return remaining; } // 直到动画完成 deferred.resolveWith( elem, [ animation ] ); return false; } // 用来计算出需要的动画数据结构 组成 animation.tweens 交给 tick 中使用 animation = deferred.promise({ ... createTween: function( prop, end ) { // 这里调用 tween 是组装数据 var tween = jQuery.Tween( ... ); animation.tweens.push( tween ); return tween; }, }) ... // 根据动画传入的参数遍历组装 animation.tweens jQuery.map( props, createTween, animation ); ... }
都是根据 Tween 这个方法进行的操作,例:
$('.ss').animate({ height: '300px', width: '100px' }, 5000).animate({ height: '100px', }, 1000)
执行第一个任务时,先看下组成的数据结构再看 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 || ( isAutoPx( prop ) ? "px" : "" ); }, ... run: function( percent ) { ... if ( this.options.duration ) { // 动画类型缓动计算 this.pos = eased = jQuery.easing[ this.easing ]( percent, this.options.duration * percent, 0, 1, this.options.duration ); } else { // 默认动画方式 this.pos = eased = percent; } // 计算当前位置 this.now = ( this.end - this.start ) * eased + this.start; // 动画进行中的回调执行 if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); } ... // 设置元素样式 进行位置修改 Tween.propHooks._default.set( this ); ... return this; } };
这个过程完成了动画的计算,根据动画的进度控制 promise 状态(进行中或者已完成、停止),在 Tween 中完成 dom 动画执行。
动画完成开启下一任务
这个就很简单了,上面的代码中也有介绍,在 tick 中通过 deferred.resolveWith 触发完成:
// 触发结果 deferred.resolveWith( elem, [ animation ] ); // 执行 animation.progress( animation.opts.progress ) .done( animation.opts.done, animation.opts.complete ) | | // 最终回到队列中取出下一个任务 继续执行 jQuery.dequeue( this, opt.queue );
动画停止
$('.ss').animate({ height: '300px', }, 5000) setTimeout(() => { // 第二个参数控制是否需要到达设定终点再停止 $('.ss').stop(false, true) }, 400)
当停止一个动画时,不指定第二个参数(到达终点再停止)时,只需要将 jQuery.timers(存储一个 queue 中的某一任务) 移除当前项即可,这样通过 requestAnimationFrame 循环执行找不到这个任务时(jQuery.fx.tick)中会停止后续执行。
当指定第二个参数时,还需要再多做一步操作,即将动画进度指定为 1(100%):
animation.tweens[ index ].run( 1 );
这样直接到达目标点,再清除 timers 中次任务。
总结
通过 队列 进行动画任务的排队,通过 promise 机制进行任务的执行顺序时机控制,再加上动画的进度和位置控制就是整个动画的设计核心组成。
jq 的源码阅读到此告一段落,没有太细节的分析,完成了几个常用模块的翻阅,任重而道远,继续努力!
1人赞