jQuery源码分析系列(40): 动画设计

前言

jQuery动画是通过animate这个API设置执行的,其内部也是按照每一个animate的划分封装了各自动画组的行为,

包括数据过滤、缓动公式、一些动画默认参数的设置、元素状态的调整、事件的处理通知机制、执行等等

换句话说,我们可以把animate看作一个对象,对象封装自己的一系列属性与方法。

jQuery可以支持连续动画,那么animate与animate之间的切换就是通过队列.queue,这个之前就已经详细的解释过了

动画的参数

jQuery的内部的方法都是针对API的处理范围设计的

我们看看Animation方法的支持情况:

.animate( properties [, duration ] [, easing ] [, complete ] )
.animate( properties, options )
  • 区别就与第二组数据的传递了,options是支持对象传参
  • properties参数就是写一个CSS属性和值的对象,动画都是涉及变化的,那么什么值才能变化?
  • 理论上来说有数值的属性都是可以变化的,width, height或者left可以执行动画,但是background-color不能,但是也不是绝对的,主要看数据的解析度,可以用插件支持
  • 除了样式属性, 一些非样式的属性,如scrollTopscrollLeft,以及自定义属性,也可应用于动画
  • 除了定义数值,每个属性能使用‘show‘, ‘hide‘, 和 ‘toggle‘。这些快捷方式允许定制隐藏和显示动画用来控制元素的显示或隐藏。为了使用jQuery内置的切换状态跟踪,‘toggle‘关键字必须在动画开始前给定属性值

简单的来说,就是把一对的参数丢大animate方法里面,然后animate就开始执行你参数规定的动画了,

那么动画每执一次就会通过回调通知告诉开发者,具体有complete/done/fail/always/step接口等等

理解定义

<img id="book"  alt="" width="100" height="123"
  style="background:red;opacity:1;position: relative; left: 500px;" />

book.animate({
    opacity: 0.25,
    left: ‘50‘,
    height: ‘toggle‘
}, {
    duration :1000,
    specialEasing: {
        height: ‘linear‘
    },
    step: function(now, fx) {
        console.log(‘step‘)
    },
    progress:function(){
        console.log(‘progress‘)
    },
    complete:function(){
        console.log(‘动画完成‘)
    }
})

首先,动画的参数都是最终值都是相对数据

如上img元素的起始

opacity是1,那么通过动画改成成0.25

left是500,那么通过动画改成成50

height为‘toggle‘ 意味着如果是隐藏与显示的自动切换

step:是针对opacity/left/height各自动画,每次改变通知三次

progress 是把opacity/left/height看成一组了,每次改变只通知一次

动画的原理

jQuery动画的原理还是很简单的,靠定时器不断的改变元素的属性

我们模拟下animate的大致实现

让元素执行一个2秒的动画到坐标为left 50的区域

animate({
    left: ‘50‘,
    duration: ‘2000‘
}

按照常规的思路,我们需要3个参数

  • 动画开始位置
  • 动画的结束位置
  • 动画的运行时间

思路一:等值变化

我们在animate内部还需要计算出当然元素的初始化布局的位置(比如500px),那么我们在2秒的时间内需变换成50px,也就是运行的路劲长就是500-50 = 450px

那么算法是不是呼之欲出了?

每毫秒移动的距离 pos = 450/2000 = 0.225px

每毫秒移动left  = 初始位置 (+/-) 每毫秒递增的距离(pos * 时间)

这样算法我们放到setInterval就会发现错的一塌糊涂,我们错最本质的东西:JS是单线程,定时器都是排队列的,理论上也达不到1ms绘制一次dom

所以每次产生的这个下一次绘制的时间差根本不是一个等比的,所以我们按照线性的等值递增是有误的

function animate(elem, options){
    //动画初始值
    var start = 500
    //动画结束值
    var end = options.left
    //动画id
    var timerId;
    var createTime = function(){
        return  (+new Date)
    }
    var startTime = createTime();
    //需要执行动画的长度
    var anminLength = start - end;
    //每13毫秒要跑的位置
    var pos = anminLength/options.time * 13
    var pre = start;
    var newValue;
    function tick(){
        if(createTime() - startTime < options.time){
            newValue = pre - pos
            //动画执行
            elem.style[‘left‘] = newValue + ‘px‘;
            pre = newValue
        }else{
            //停止动画
            clearInterval(timerId);
            timerId = null;
            console.log(newValue)
        }
    }
    //开始执行动画
    var timerId = setInterval(tick, 13);
}

思路一实现:


<!doctype html><img id="book" style="background:red;opacity:1;position: relative; left: 500px;" width="100" height="123" data-mce-style="background: red; opacity: 1; position: relative; left: 500px;" /><script type="text/javascript">

var book = document.getElementById(‘book‘)

animate(book, {
left: 50,
time: 2000
})

function animate(elem, options){
//动画初始值
var start = 500
//动画结束值
var end = options.left
//动画id
var timerId;

var createTime = function(){
return (+new Date)
}

var startTime = createTime();

//需要执行动画的长度
var anminLength = start - end;

//每13毫秒要跑的位置
var pos = anminLength/options.time * 13

var pre = start;
var newValue;

function tick(){
if(createTime() - startTime < options.time){
newValue = pre - pos
//动画执行
elem.style[‘left‘] = newValue + ‘px‘;
pre = newValue
}else{
//停止动画
clearInterval(timerId);
timerId = null;
console.log(newValue)
}
}

//开始执行动画
var timerId = setInterval(tick, 13);
}

</script>

思路二:动态计算

setInterval的调用是不规律的,但是调用的时间是(2秒)是固定的,我们可以在每次调用的时候算法时间差的比值,用这个比值去计算移动的距离就比较准确了

remaining = Math.max(0, startTime + duration - currentTime),

通过这个公司我们计算出,每次setInterval调用的时候,当前时间在总时间中的一个位置

remaining

看到没有,这个值其实很符合定时器的特性,也是一个没有规律的值

根据这个值,我们可以得出当前位置的一个百分比了

var remaining = Math.max(0, startTime + options.duration - createTime())
var temp = remaining / options.duration || 0;
var percent = 1 - temp;

pecent

那么这个移动的距离就很简单了

我把整个公式就直接列出来了

var createTime = function(){
    return  (+new Date)
}
//元素初始化位置
var startLeft = 500;
//元素终点位置
var endLeft = 50;
//动画运行时间
var duration = 2000;
//动画开始时间
var startTime = createTime();

function tick(){
    //每次变化的时间
    var remaining = Math.max(0, startTime + duration - createTime())
    var temp = remaining / duration || 0;
    var percent = 1 - temp;
    //最终每次移动的left距离
    var leftPos  = (endLeft- startLeft) * percent +startLeft;
}

//开始执行动画
setInterval(tick, 13);

leftPos就是每次移动的距离了,基本上比较准确了,事实上jQuery内部也就是这么干的

这里13代表了动画每秒运行帧数,默认是13毫秒。属性值越小,在速度较快的浏览器中(例如,Chrome),动画执行的越流畅,但是会影响程序的性能并且占用更多的 CPU 资源

在新的游览器中,我们都可以采用requestAnimationFrame更优

思路二实现:

<!doctype html><img id="book" style="background:red;opacity:1;position: relative; left: 500px;" width="100" height="123" data-mce-style="background: red; opacity: 1; position: relative; left: 500px;" /><script type="text/javascript">

var book = document.getElementById(‘book‘)

animate(book, {
left: 50,
duration: 2000
})

function animate(elem, options){
//动画初始值
var start = 500
//动画结束值
var end = options.left
//动画id
var timerId;
var createTime = function(){
return (+new Date)
}
//动画开始时间
var startTime = createTime();

function tick(){
//每次变化的时间
var remaining = Math.max(0, startTime + options.duration - createTime())
var temp = remaining / options.duration || 0;
var percent = 1 - temp;
var stop = function(){
//停止动画
clearInterval(timerId);
timerId = null;
}
var setStyle = function(value){
elem.style[‘left‘] = value + ‘px‘
}
//移动的距离
var now = (end - start) * percent + start;
if(percent === 1){
setStyle(now)
stop();
}else{
setStyle(now)
}
}

//开始执行动画
var timerId = setInterval(tick, 13);
}
</script>

动画的扩展

知道动画处理的基本原理与算法了,那么jQuery在这个基础上封装扩展,让动画使用起来更灵活方便

我归纳有几点:

  • 参数的多形式传递
  • 基于promise的事件反馈
  • 增加属性的show/hide/toggle的快捷方式
  • 可以给css属性设置独立的缓动函数

基于promise的事件通知

得益于deferred的机制,可以让一个对象转化成带有promise的特性,实现了done/fail/always/progress等等一系列的事件反馈接口

这样的设计我们并不陌生在ready、ajax包括动画都是基于这样的异步模型的结构

deferred = jQuery.Deferred()
//生成一个动画对象了
animation = deferred.promise({}) //混入动画的属性与方法

那么这样操作的一个好处就是,可以把逻辑处理都放到一块

我们在代码的某一个环节针对特别的处理,需要临时改变一些东西,但是在之后我们希望又恢复原样,为了逻辑的清晰,我们可以引入deferred.alway方法

在某一个环节改了一个属性,然后注册到alway方法上一个完成的回调用来恢复,这样的逻辑块是很清晰的

style.overflow = "hidden";
    anim.always(function() {
        //完成后恢复溢出
        style.overflow = opts.overflow[0];
        style.overflowX = opts.overflow[1];
        style.overflowY = opts.overflow[2];
    });

增加属性的show/hide/toggle的快捷方式

指定中文参数是比较特殊的,这种方式也是jQuery自己扩展的行为,逻辑上也很容易处理

ook.animate({
      left: ‘50‘,
      height:‘hide‘
},

height高度在动画结束之后隐藏元素,那么意味着元素本身的高度height也是需要改变的从初始的位置慢慢的递减到0然后隐藏起来

代码中有这么一段,针对hide的动作,我们在done之后会给元素直接隐藏起来

//目标是显示
if (hidden) {
    jQuery(elem).show();
} else {
    //目标是隐藏
    anim.done(function() {
        jQuery(elem).hide();
    });
}

其实show与hide的流程是一样的,只是针对元素在初始与结束的一个状态的改变

css属性设置独立的缓动函数

在动画预初始化之后(为了支持动画,临时改变元素的一些属性与状态),我们就需要给每一个属性生成一个独立的缓动对象了createTween,主要用于封装这个动画的算法与执行的一些流程操作控制

//////////////////
//生成对应的缓动动画 //
//////////////////
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;
},

tween对象

通过这个结构大概就知道了,这个就是用于生成动画算法所需要的一些数据与算法的具体流程控制了

属性预处理

  • 针对height/width动画的时候,要先处理本身元素溢出
  • 针对height/width动画的时候,元素本身的inline状态处理

我们知道元素本身在布局的时候可以用很多属性对其设置,可是一旦进行动画化的话,某些属性的设置可能会对动画的执行产生副作用,所以针对这样的属性,jQuery直接在内部做了最优的处理

如果我们进行元素height/width变化的时候,比如height:1,这样的处理jQuery就需要针对元素做一些强制性的处理

1 添加overflow =“hidden”
2.如果设置了内联并且没有设置浮动 display = "inline-block";

因为内容溢出与内联元素在执行动画的时候,与这个height/width的逻辑是符合的

当然针对这样的修改jQuery非常巧妙了用到了deferred.always方法,我们在执行动画的时候,由于动画的需要改了原始的属性,但是动画在结束之后,我们还是需要还原成其状态

deferred量身定做的always方法,不管成功与失败都会执行这个复原的逻辑

//设置溢出隐藏
if (opts.overflow) {
    style.overflow = "hidden";
    anim.always(function() {
        //完成后恢复溢出
        style.overflow = opts.overflow[0];
        style.overflowX = opts.overflow[1];
        style.overflowY = opts.overflow[2];
    });
}

总结

通过上面不难看出,jQuery动画其实原理上本身是复杂的。量变产生质变,通过扩展大量的便捷方式加大了逻辑上的难度,但是从根本上来说:

主要包括:

  • 属性过滤specialEasing处理的propFilter方法
  • 通过Deferred生成流程控制体系
  • 通过defaultPrefilter方法对动画执行的临时修正
  • 通过createTween方法,生成动画的算法与流程控制器
  • 最后通过setInterval来控制每一个createTween对象的执行

大体上jQuery的动画就这么些内容,当然还有一些细节的话 遇到在提出来了,下章就会通过上面的这些处理,实现一个类jquery动画的模拟了,加强理解

时间: 2024-10-29 19:08:26

jQuery源码分析系列(40): 动画设计的相关文章

jQuery源码分析系列(39) : 动画队列

data函数在jQuery中只有短短的300行代码,非常不起点 ,剖析源码的时候你会发现jQuery只要在有需要保存数据的地方无时无刻不依赖这个基础设施 动画会调用队列,队列会调用data数据接口还保存队列里面的的动画数据 所以我们在自习回顾下关于数据缓存 //These may be used throughout the jQuery core codebase //存数据的 //用户使用 data_user = new Data(); //存储对象 //jQuery内部私有 //用来存事件

[转]jQuery源码分析系列

文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaron/jQuery 正在编写的书 - jQuery架构设计与实现 本人在慕课网的教程(完结) jQuery源码解析(架构与依赖模块) 64课时 jQuery源码解析(DOM与核心模块)64课时 jQuery源码分析目录(完结) jQuery源码分析系列(01) : 整体架构 jQuery源码分析系列(

jQuery源码分析系列(38) : 队列操作

Queue队列,如同data数据缓存与Deferred异步模型一样,都是jQuery库的内部实现的基础设施 Queue队列是animate动画依赖的基础设施,整个jQuery中队列仅供给动画使用 Queue队列 队列是一种特殊的线性表,只允许在表的前端(队头)进行删除操作(出队),在表的后端(队尾)进行插入操作(入队).队列的特点是先进先出(FIFO-first in first out),即最先插入的元素最先被删除. 为什么要引入队列? 我们知道代码的执行流有异步与同步之分,例如 var a

jQuery源码分析系列(34) : Ajax - 预处理jsonp

上一章大概讲了前置过滤器和请求分发器的作用,这一章主要是具体分析每种对应的处理方式 $.ajax()调用不同类型的响应,被传递到成功处理函数之前,会经过不同种类的预处理(prefilters). 预处理的类型取决于由更加接近默认的Content-Type响应,但可以明确使用dataType选项进行设置.如果提供了dataType选项, 响应的Content-Type头信息将被忽略. 有效的数据类型是text, html, xml, json,jsonp,和 script. dataType:预期

jQuery源码分析系列(31) : Ajax deferred实现 - Aaron.

AJAX的底层实现都是浏览器提供的,所以任何基于api上面的框架或者库,都只是说对于功能的灵活与兼容维护性做出最优的扩展 ajax请求的流程: 1.通过 new XMLHttpRequest 或其它的形式(指IE)生成ajax的对象xhr. 2.通过xhr.open(type, url, async, username, password)的形式建立一个连接. 3.通过setRequestHeader设定xhr的请求头部(request header). 4.通过send(data)请求服务器端

jQuery源码分析系列(33) : AJAX中的前置过滤器和请求分发器

jQuery1.5以后,AJAX模块提供了三个新的方法用于管理.扩展AJAX请求,分别是: 1.前置过滤器 jQuery. ajaxPrefilter 2.请求分发器 jQuery. ajaxTransport, 3.类型转换器 ajaxConvert 源码结构: jQuery.extend({ /** * 前置过滤器 * @type {[type]} */ ajaxPrefilter: addToPrefiltersOrTransports(prefilters), /** * 请求分发器 *

jQuery源码分析系列(36) : Ajax - 类型转化器

什么是类型转化器? jQuery支持不同格式的数据返回形式,比如dataType为 xml, json,jsonp,script, or html 但是浏览器的XMLHttpRequest对象对数据的响应只有 responseText与responseXML 二种 所以现在我要定义dataType为jsonp,那么所得的最终数据是一个json的键值对,所以jQuery内部就会默认帮你完成这个转化工作 jQuery为了处理这种执行后数据的转化,就引入了类型转化器,如果没有指定类型就依据响应头Con

jQuery源码分析系列(35) : Ajax - jsonp的实现与原理

ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本 json核心就是:允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了. jquery ext dojo这类库的实现手段其实大同小异 在同源策略下,在某个服务器下的页面是无法获取到该服务器以外的数据的,但img.iframe.s

jQuery源码分析系列(37) : Ajax 总结

综合前面的分析,我们总结如下3大块: jQuery1.5以后,AJAX模块提供了三个新的方法用于管理.扩展AJAX请求 前置过滤器 jQuery. ajaxPrefilter 请求分发器 jQuery. ajaxTransport 类型转换器 ajaxConvert 为了整体性与扩展性考虑,把整个结构通过Deferred实现异步链式模型,Promise对象可以轻易的绑定成功.失败.进行中三种状态的回调函数,然后通过在状态码在来回调不同的函数就行了 出于同源策略考虑,存在跨域问题,所以ajax内部