jQuery源码解析(5)—— Animation动画

本文深入解析jQuery的动画机制,包括全局Interval、同步与异步操作。介绍了jQuery.fn.animate、jQuery.fn.stop/finish的工作原理,以及Animation动画、Tween和如何处理toggle/show/hide的动画。通过分析jQuery源码,理解其动画实现细节和优化措施。
摘要由CSDN通过智能技术生成

闲话

jQuery的动画机制有800行, 虽然不如样式的1300行,难度上却是不减。由于事前不了解animate接口的细节使用规则,看代码期间吃了很多苦头,尤其是深恶痛绝的defaultPrefilter函数,靠着猜想和数次的逐行攻略,终于全部拿下。本文将一点一点拆解出jq的动画机制及具体实现,解析各种做法的目的、解耦的方式、必要的结构、增强的辅助功能。

需要提前掌握queue队列的知识,css样式机制、data缓存、deferred对象至少要了解核心API的功能。有兴趣可以参考我之前的几篇分析。

(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)

动画机制

jQuery的动画机制比较复杂,下面将逐一分析其中要点。

全局Interval

教学时常用的动画函数demo,结构是下面这样:

/* demo */

// json -> { prop1: end1, prop2: end2 ...} 属性与终点的名值对,可一次运动多属性
function Animation( elem, json, duration, callback ) {
   

    // some code...

    // 每步运动
    var tick = function() {
   
        // 对每个属性算出每步值,并设置。到终点时取消定时器,并执行回调 callback()
    };
    elem.timer = setInterval(tick, 20);
}

如何计算每步运动的值需要讲究,举个栗子:

// 方式 1
// 计算次数,算出每次增量(与定时器的设置时间,严格相关)
times = duration / 20;
everyTimeAddNum = ( end - start ) / timers;

// 方式 2
// 计算当前流逝的比例,根据比例设置最终值(有定时器即可,与定时时间无关)
passTime = ( +new Date() - createTime ) / duration;
passTime = passTime > 1 ? 1 : passTime;
toValue = ( end - start ) * passTime + start;

方式2为标准的使用法则,方式1虽然很多人仍在使用包括教学,但是会出现如下两个问题:

问题1:js单线程

javascript是单线程的语言,setTimeout、setInterval定时的向语言的任务队列添加执行代码,但是必须等到队列中已有的代码执行完毕,若遇到长任务,则拖延明显。对于”方式1”,若在tick内递归的setTimout,tick执行完才会再次setTimeout,每次的延迟都将叠加无法被追偿。setInterval也不能幸免,因为js引擎在使用setInterval()时,仅当队列里没有当前定时器的任何其它代码实例时,才会被添加,而次数和值的累加都是需要函数执行才会生效,因此延迟也无法被追偿。

问题2:计时器精度

浏览器并不一定严格按照设置的时间(比如20ms)来添加下一帧,IE8及以下浏览器的精度为15.625ms,IE9+、Chrome精度为4ms,ff和safari约为10ms。对于“方式1”这种把时间拆为确定次数的计算方式,运动速度就一点不精确了。

jQuery显然采用了”方式2”,而且优化了interval的调用。demo中的方式出现多个动画时会造成 interval 满天飞的情况,影响性能,既然方式2中动画逻辑与定时器的时间、调用次数无关,那么可以单独抽离,整个动画机制只使用一个统一的setInterval,把tick推入堆栈jQuery.timers,每次定时器调用jQuery.fx.tick()遍历堆栈里的函数,通过tick的返回值知道是否运动完毕,完毕的栈出,没有动画的时候就jQuery.fx.stop()暂停。jQuery.fx.start()开启定时器前会检测是开启状态,防止重复开启。每次把tick推入堆栈的时候都会调用jQuery.fx.start()。这样就做到了需要时自动开启,不需要时自动关闭。

[源码]

// #672
// jQuery.timers 当前正在运动的动画的tick函数堆栈
// jQuery.fx.timer() 把tick函数推入堆栈。若已经是最终状态,则不加入
// jQuery.fx.interval 唯一定时器的定时间隔
// jQuery.fx.start() 开启唯一的定时器timerId
// jQuery.fx.tick() 被定时器调用,遍历timers堆栈
// jQuery.fx.stop() 停止定时器,重置timerId=null
// jQuery.fx.speeds 指定了动画时长duration默认值,和几个字符串对应的值

// jQuery.fx.off 是用在确定duration时的钩子,设为true则全局所有动画duration都会强制为0,直接到结束状态

// 所有动画的"每步运动tick函数"都推入timers
jQuery.timers = [];

// 遍历timers堆栈
jQuery.fx.tick = function() {
   
    var timer,
        timers = jQuery.timers,
        i = 0;

    // 当前时间毫秒
    fxNow = jQuery.now();

    for ( ; i < timers.length; i++ ) {
        timer = timers[ i ];

        // 每个动画的tick函数(即此处timer)执行时返回remaining剩余时间,结束返回false
        // timers[ i ] === timer 的验证是因为可能有瓜娃子在tick函数中瞎整,删除jQuery.timers内项目
        if ( !timer() && timers[ i ] === timer ) {
            timers.splice( i--, 1 );
        }
    }

    // 无动画了,则stop掉全局定时器timerId
    if ( !timers.length ) {
        jQuery.fx.stop();
    }
    fxNow = undefined;
};

// 把动画的tick函数加入$.timers堆栈
jQuery.fx.timer = function( timer ) {
   
    jQuery.timers.push( timer );
    if ( timer() ) {
        jQuery.fx.start();
    // 若已经在终点了,无需加入
    } else {
        jQuery.timers.pop();
    }
};

// 全局定时器定时间隔
jQuery.fx.interval = 13;

// 启动全局定时器,定时调用tick遍历$.timers
jQuery.fx.start = function() {
   
    // 若已存在,do nothing
    if ( !timerId ) {
        timerId = window.setInterval( jQuery.fx.tick, jQuery.fx.interval );
    }
};

// 停止全局定时器timerId
jQuery.fx.stop = function() {
   
    window.clearInterval( timerId );
    timerId = null;
};

// speeds(即duration)默认值,和字符串的对应值
jQuery.fx.speeds = {
    slow: 600,
    fast: 200,

    // Default speed,默认
    _default: 400
};


同步、异步

jQuery.fn.animate

jQuery动画机制最重要的一个考虑是:动画间便捷的同步、异步操作。

jQuery允许我们通过$().animate()的形式调用,对应的外观方法是jQuery.fn.animate( prop, speed, easing, callback ),内部调用动画的核心函数Animation( elem, properties, options )

上面的demo虽然粗糙,但是思路一致。Animation一经调用,内部的tick函数将被jQuery.fx.timer函数推入jQuery.timers堆栈,立刻开始按照jQuery.fx.interval的间隔运动。要想使动画异步,就不能立即调用Animation。在回调callback中层层嵌套来完成异步,显然是极不友好的。jQuery.fn.animate中使用了queue队列,把Animation函数的调用封装在doAnimation函数中,通过把doAnimation推入指定的队列,按照队列顺序异步触发doAnimation,从而异步调用Animation。

queue队列是一个堆栈,比如elem的”fx”队列,jQuery.queue(elem, “fx”)即为缓存jQuery._data(elem, “fxqueue”)。每个元素的”fx”队列都是不同的,因此不同元素或不同队列之间的动画是同步的,相同元素且相同队列之间的动画是异步的。添加到”fx”队列的函数若是队列中当前的第一个函数,将被直接触发,而添加到其他队列中的函数需要手动调用jQuery.dequeue才会启动执行。

如何设置添加的队列呢?jQuery.fn.animate支持对象参数写法jQuery.fn.animate( prop, optall),通过 optall.queue指定队列,未指定队列的按照默认值”fx”处理。speed、easing、callback均不是必须项,内部通过jQuery.speed将参数统一为对象optall。optall会被绑定上被封装过的optall.complete函数,调用后执行dequeue调用队列中下一个doAnimation(后面会讲Animation执行完后如何调用complete自动执行下一个动画)

虽然加入了queue机制后,默认的动画顺序变为了异步而非同步。但optall.queue指定为false时,不使用queue队列机制,doAnimation将立即调用Animation执行动画,保留了原有的同步机制。

/* #7888 jQuery.speed
 * 设置参数统一为options对象
---------------------------------------------------------------------- */
// 支持的参数类型(均为可选参数,只有fn会参数提前。无speed设为默认值,无easing在Tween.prototype.init中设为默认值)
// (options)
// (speed [, easing | fn])
// (speed, easing, fn)
// (speed)、(fn)
jQuery.speed = function( speed, easing, fn ) {
   
    var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
        complete: fn || !fn && easing ||
            jQuery.isFunction( speed ) && speed,
        duration: speed,
        easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
    };

    // jQuery.fx.off控制全局的doAnimation函数生成动画的时长开关
    opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
        // 支持 "slow" "fast",无值则取默认400
        opt.duration in jQuery.fx.speeds ?
            jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;

    // true/undefined/null -> 设为默认队列"fx"
    // false不使用队列机制
    if ( opt.queue == null || opt.queue === true ) {
        opt.queue = "fx";
    }

    opt.old = opt.complete;

    // 对opt.complete进行再封装
    // 目的是该函数可以dequeue队列,让队列中下个doAnimation开始执行
    opt.complete = function() {
   
        // 非函数或无值则不调用
        if ( jQuery.isFunction( opt.old ) ) {
            opt.old.call( this );
        }

        // false不使用队列机制
        if ( opt.queue ) {
            jQuery.dequeue( this, opt.queue );
        }
    };

    return opt;
};


/* #7930 jQuery.fn.animate
 * 外观方法,对每个elem添加动画到队列(默认"fx"队列,为false不加入队列直接执行)
---------------------------------------------------------------------- */
jQuery.fn.animate = function( prop, speed, easing, callback ) {
   
    // 是否有需要动画的属性
    var empty = jQuery.isEmptyObject( prop ),
        // 参数修正到对象optall
        optall = jQuery.speed( speed, easing, callback ),
        doAnimation = function() {
   

            // 执行动画,返回一个animation对象(后面详细讲)
            var anim = Animation( this, jQuery.extend( {}, prop ), optall );

            // jQuery.fn.finish执行期间jQuery._data( this, "finish" )设置为"finish",所有动画创建后都必须立即结束到end,即直接运动到最终状态(后面详细讲)
            if ( empty || jQuery._data( this, "finish" ) ) {
                anim.stop( true );
            }
        };
        // 用于jQuery.fn.finish方法内判断 queue[ index ] && queue[ index ].finish。比如jQuery.fn.delay(type)添加到队列的方法没有finish属性,不调用直接舍弃
        doAnimation.finish = doAnimation;

    return empty || optall.queue === false ?
        // 直接遍历执行doAnimation
        this.each( doAnimation ) :
        // 遍历元素把doAnimation加入对应元素的optall.queue队列
        this.queue( optall.queue, doAnimation );
};


jQuery.fn.stop/finish

现在我们有了同步、异步两种方式,但在同步的时候,有可能出现重复触发某元素动画,而我们并不需要。在jq中按照场景可分为:相同队列正在运动的动画、所有队列正在运动的动画、相同队列所有的动画、所有队列的动画、非队列正在运动的动画。停止动画分为两种状态:直接到运动结束位置、以当前位置结束。

实现原理

清空动画队列,调用$(elems).queue( type, [] ),会替换队列为[],也可以事先保存队列,然后逐个执行,这正是jQuery.fn.finish的原理。停止当前动画,jQuery.timers[ index ].anim.stop( gotoEnd )。gotoEnd为布尔值,指定停止动画到结束位置还是当前位置,通过timers[ index ].elem === this && timers[ index ].queue === type匹配队列和元素,从这里也能看出Animation函数中的单步运动tick函数需要绑定elem、anim、queue属性(anim是Animation返回的animation对象,stop函数用来结束当前动画,后面会详细讲)。

然而并不是添加到队列的都是doAnimation,比如jQuery.fn.delay(),由于没调用Animation,所以没有tick函数,自然没有anim.stop,从jq源码中可以看出,推荐在队列的hook上绑定hooks.stop停止函数(因此stop/finish中会调用hooks.stop)。queue队列中被执行的函数备注了的next函数(dequeue操作,调用下一个)和对应的hook对象($._data(type+’queuehook’)缓存,empty.fire用于自毁)和this(元素elem),因此可以通过next调用下一项。

/* #8123 jQuery
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值