之前自己在做js动画组件时遇到了JS时钟精度问题,遂去参考下jQuery的处理方式,顺便把jQuery的动画组件部分简要分析了一遍。
jQuery组件都是由一个API接口函数暴露给用户的,组件的核心功能由有底层函数去完成。JQuery动画组件的接口函数就是animate。在了解animate之前,先了解到jQuery中动画相关的几个属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // 用来设置定时器的变量 var timerId ; jQuery.extend({ // 修正一些参数,比如在回调函数中加入队列控制,并把这些修正后的参数放置一个对象里面返回 speed: function( speed, easing, fn ) {}, // 缓冲函数,决定动画运行方式 easing: {}, // 用来存储 执行动画实例单步的方法 的数组(有点拗口) timers: [], // 动画构造函数 fx: function( elem, options, prop ) {} }); |
结合我在源码中加的一些注释,简要描述jQuery的动画过程:
animate方法,首先调用speed方法去修正参数,然后用枚举或者队列的方式去执行多个动画。接下来,遍历动画中要修改的属性,修正属性值,并将每个属性生成一个动画实例。调用实例custom方法将属性从开始值过渡到结束值。
custom方法,把动画的单步操作(step方法)压入到前面提到的timers数组中,并调用tick遍历执行所有动画。
step方法——动画的单步操作,计算动画的逝去时间,并将逝去时间与预定动画时长的比值作为动画的变化比值,将比比值结合动画缓冲函数,计算出动画变化值。如果动画逝去时间超过预定动画时长,则将属性更新到最后一帧,并调用预定的回调函数。返回false,表示该动画实例已完成。
tick方法——定时遍历执行所有动画实例的step方法。如果step方法返回为false(该动画实例已完成),则将包含执行step方法的函数从timers数组中移除,如果timers数组为空(全部动画实例都运行完毕),执行stop方法。
stop方法——终止动画,停止定时器,并清空定时器变量timerId
看完jQuery源码,自己也改造了一个。满足最基本的功能呢 查看Demo
下面是JQuery动画组件的大部分源码(我加了一些注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | jQuery.fn.extend({ // 动画组件接口函数(四个参数分别是要变化的属性以及属性值, 动画时长, 动画缓冲方法, 动画完成的回调函数) animate: function( prop, speed, easing, callback ) { // 调用speed方法修正参数 var optall = jQuery.speed(speed, easing, callback); // 如果属性是空对象,则调用回调函数 if ( jQuery.isEmptyObject( prop ) ) { return this.each( optall.complete ); } // 枚举或者队列的方式来执行多个动画 return this[ optall.queue === false ? "each" : "queue" ](function() { var opt = jQuery.extend({}, optall), p, isElement = this.nodeType === 1, hidden = isElement && jQuery(this).is(":hidden"), self = this; // 遍历属性 修正参数 for ( p in prop ) { // 将属性名修改为驼峰命名 var name = jQuery.camelCase( p ); if ( p !== name ) { prop[ name ] = prop[ p ]; delete prop[ p ]; p = name; } // 如果当前状态已经是目标状态 则调用回调函数 if ( prop[p] === "hide" && hidden || prop[p] === "show" && !hidden ) { return opt.complete.call(this); } // 如果修改DOM的高度或者宽度属性,则添加相应的css属性 (比如overflow,display等) // 太智能了,算不算过渡设计? if ( isElement && ( p === "height" || p === "width" ) ) { opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ]; if ( jQuery.css( this, "display" ) === "inline" && jQuery.css( this, "float" ) === "none" ) { if ( !jQuery.support.inlineBlockNeedsLayout ) { this.style.display = "inline-block"; } else { var display = defaultDisplay(this.nodeName); if ( display === "inline" ) { this.style.display = "inline-block"; } else { this.style.display = "inline"; this.style.zoom = 1; } } } } // 属性为数组? 我是土人,没见到css属性为数组的。或者我领悟错了 if ( jQuery.isArray( prop[p] ) ) { (opt.specialEasing = opt.specialEasing || {})[p] = prop[p][1]; prop[p] = prop[p][0]; } } if ( opt.overflow != null ) { this.style.overflow = "hidden"; } // 把属性对象作为opt的一个属性存储起来 opt.curAnim = jQuery.extend({}, prop); // 遍历属性 执行每个属性的动画 jQuery.each( prop, function( name, val ) { // 为每个属性生成一个动画实例 var e = new jQuery.fx( self, opt, name ); // 如果是使用预定义的动画比如 toggle,show或者hide的,直接调用对应的方法 if ( rfxtypes.test(val) ) { e[ val === "toggle" ? hidden ? "show" : "hide" : val ]( prop ); } else { // 修正变化值 var parts = rfxnum.exec(val), // 获取当前属性值 start = e.cur() || 0; if ( parts ) { var end = parseFloat( parts[2] ), unit = parts[3] || "px"; if ( unit !== "px" ) { jQuery.style( self, name, (end || 1) + unit); start = ((end || 1) / e.cur()) * start; jQuery.style( self, name, start + unit); } // 处理相对动画 if ( parts[1] ) { end = ((parts[1] === "-=" ? -1 : 1) * end) + start; } // 调用动画实例的custom方法(使属性从开始值变化到最终值) e.custom( start, end, unit ); } else { e.custom( start, val, "" ); } } }); return true; }); } }); jQuery.extend({ // 修正参数 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 }; // 修正easing参数 opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default; // 在回调函数中加入队列控制 opt.old = opt.complete; opt.complete = function() { if ( opt.queue !== false ) { jQuery(this).dequeue(); } if ( jQuery.isFunction( opt.old ) ) { opt.old.call( this ); } }; // 返回包含各种参数的对象 return opt; }, // 动画缓冲函数 easing: { // 线性 linear: function( p, n, firstNum, diff ) { // 匀速直线运动 s = s0 + vt return firstNum + diff * p; }, swing: function( p, n, firstNum, diff ) { return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum; } }, // 用来存储 执行动画实例单步的方法 的数组(有点拗口) timers: [], // 动画构造函数 fx: function( elem, options, prop ) { this.options = options; this.elem = elem; this.prop = prop; if ( !options.orig ) { options.orig = {}; } } }); jQuery.fx.prototype = { // 更新css属性 update: function() { if ( this.options.step ) { this.options.step.call( this.elem, this.now, this ); } (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); }, // 获取当前属性值 cur: function() { if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) { return this.elem[ this.prop ]; } var r = parseFloat( jQuery.css( this.elem, this.prop ) ); return r && r > -10000 ? r : 0; }, // 使属性从开始值变化到最终值 custom: function( from, to, unit ) { var self = this, fx = jQuery.fx; // 把当前时间储存起来 this.startTime = jQuery.now(); this.start = from; this.end = to; this.unit = unit || this.unit || "px"; this.now = this.start; this.pos = this.state = 0; function t( gotoEnd ) { // 调用动画实现的单步处理方法 return self.step(gotoEnd); } t.elem = this.elem; // 初次执行动画实例的单步 // 并将执行单步的方法压入timers数组(前面有解释过timers的作用) // 设置动画定时器,定时调用tick方法(一个动画中有且只能有一个定时器) if ( t() && jQuery.timers.push(t) && !timerId ) { timerId = setInterval(fx.tick, fx.interval); } }, // 动画单步 step: function( gotoEnd ) { // 获取当前时间 var t = jQuery.now(), done = true; // 如果手动结束动画 或者 动画逝去时间大于等于预定动画时长,则结束动画 if ( gotoEnd || t >= this.options.duration + this.startTime ) { // 将动画更新到最后一步 this.now = this.end; this.pos = this.state = 1; this.update(); // 设置当前动画实例结束的标志 this.options.curAnim[ this.prop ] = true; for ( var i in this.options.curAnim ) { if ( this.options.curAnim[i] !== true ) { done = false; } } // 如果全部动画实例都结束了 则重置相应css属性(比如overflow等),并调用回调函数 if ( done ) { if ( this.options.overflow != null && !jQuery.support.shrinkWrapBlocks ) { var elem = this.elem, options = this.options; jQuery.each( [ "", "X", "Y" ], function (index, value) { elem.style[ "overflow" + value ] = options.overflow[index]; } ); } if ( this.options.hide ) { jQuery(this.elem).hide(); } if ( this.options.hide || this.options.show ) { for ( var p in this.options.curAnim ) { jQuery.style( this.elem, p, this.options.orig[p] ); } } this.options.complete.call( this.elem ); } // 返回false return false; } else { // 计算动画逝去时间 var n = t - this.startTime; // 计算动画变化比值 // JQuery每一步的变化值是根据动画逝去时间来计算的,这样来弥补XP系统IE下JS时钟精度问题 this.state = n / this.options.duration; // 根据变化比值,结合动画缓冲函数计算出本帧变化值 var specialEasing = this.options.specialEasing && this.options.specialEasing[this.prop]; var defaultEasing = this.options.easing || (jQuery.easing.swing ? "swing" : "linear"); this.pos = jQuery.easing[specialEasing || defaultEasing](this.state, n, 0, 1, this.options.duration); this.now = this.start + ((this.end - this.start) * this.pos); // 更新css属性 this.update(); } return true; } }; jQuery.extend( jQuery.fx, { // 执行动画 tick: function() { var timers = jQuery.timers; // 遍历执行动画实例的单步方法 for ( var i = 0; i < timers.length; i++ ) { // timers[i]就是custom中定义的函数t,用来执行动画实例的单步 // 如果单步返回为false(表明该动画实例已运行完毕),则将其从timers数组中移除 if ( !timers[i]() ) { timers.splice(i--, 1); } } // 如果timers数组为空(表明全部动画实例都运行完毕了),执行stop if ( !timers.length ) { jQuery.fx.stop(); } }, interval: 13, // 停止定时器,并清空定时器变量 stop: function() { clearInterval( timerId ); timerId = null; }, // 预设的speed取值 speeds: { slow: 600, fast: 200, _default: 400 } }); |
好久没开过FLASH了,这些代码都看不懂了
额 我居然给spam reply
AG