【jq 源码解析6-动画】

jq源码 | 2020-12-31 15:24:49 370次 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 进入时,队列中存在一个进行中和刚传入的任务,第一个任务执行完毕再取出第二个执行,保证了动画的执行是连续的串行执行。


队列

上面提到了 queuedequeue,队列是一种先进先出的数据结构,大概了解了动画的一个执行流程控制,这里看下队列的内部设计:

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 的代码设计:

微信截图_20210107203718.png

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人赞

分享到: