[转载]jQuery1.9.1源码分析–Animation模块 – LukeLin – 博客园.
JQuery1.9.1源码分析–Animation模块
var fxNow, // 使用一个ID来执行动画setInterval timerId, rfxtypes = /^(?:toggle|show|hide)$/, // eg: +=30.5px // 执行exec匹配["+=30.5px", "+", "30.5", "px"] rfxnum = new RegExp('^(?:([+-])=|)(' + core_pnum + ')([a-z%]*)$', 'i'), // 以“queueHooks”结尾 rrun = /queueHooks$/, animationPrefilters = [defaultPrefilter], tweeners = { // 在动画前再次对动画参数做调整 '*': [ function(prop, value) { var end, unit, // this指向animation对象 // 返回一个Tween构造函数实例 tween = this.createTween(prop, value), // eg:["+=30.5px", "+", "30.5", "px"] parts = rfxnum.exec(value), // 计算当前属性样式值 target = tween.cur(), start = +target || 0, scale = 1, maxIterations = 20; if (parts) { // 数值 end = +parts[2]; // 单位 // jQuery.cssNumber里面的值是不需要单位的 unit = parts[3] || (jQuery.cssNumber[prop] ? '' : 'px'); // We need to compute starting value // 我们需要计算开始值 if (unit !== 'px' && start) { // Iteratively approximate from a nonzero starting point // Prefer the current property, because this process will be trivial if it uses the same units // Fallback to end or a simple constant // 尝试从元素样式中获取开始值 start = jQuery.css(tween.elem, prop, true) || end || 1; do { // If previos iteration zeroed out, double until we get *something* // Use a string for doubling factor so we don't accidentally see scale as unchanged below scale = scale || '.5'; // Adjust and apply start = start / scale; jQuery.style(tween.elem, prop, start + unit); // Update scale, tolerating zero or NaN from tween.cur() // And breaking the loop if scale is unchanged or perfect. or if we've just had enough } while (scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations); } tween.unit = unit; tween.start = start; // If a +=/-= token was provided, we're doing a relative animation tween.end = parts[1] ? start + (parts[1] + 1) * end : end; } return tween; } ] }; // Animations created synchronous will run synchronously // TODO // 返回一个时间戳,然后用setTimeout延时将fxNow设置为undefined function createFxNow() { setTimeout(function() { fxNow = undefined; }); return (fxNow = jQuery.now()); } function createTweens(animation, props) { // 遍历props动画属性对象,并执行回调 jQuery.each(props, function(prop, value) { // 如果tweeners[prop]数组存在,将它和tweeners['*']连接 var collection = (tweeners[prop] || []).concat(tweeners['*']), index = 0, length = collection.length; // 遍历函数数组 for (; index < length; index++) { // 如果该函数有返回值,且==true,退出函数 if (collection[index].call(animation, prop, value)) { // We're done with this property return; } } }); } function Animation(elem, properties, options) { var result, stopped, index = 0, length = animationPrefilters.length, // deferred无论成功还是失败都会删除elem元素 deferred = jQuery.Deferred().always(function() { // don't match elem in the :animated selector // 在“:animated”选择器中不会匹配到它们 delete tick.elem; }), tick = function() { if (stopped) { return false; } var // 计算当前动画时间戳 currentTime = fxNow || createFxNow(), // 结束时间减当前时间,计算出剩余时间 remaining = Math.max(0, animation.startTime + animation.duration - currentTime), // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) // 剩余时间百分比 temp = remaining / animation.duration || 0, // 已执行百分比 percent = 1 - temp, index = 0, // 动画属性对应的tweens length = animation.tweens.length; // 遍历tweens,并执行对应的run方法,将已执行百分比通过传参传入 // run方法通过缓动算法计算出样式值,然后应用到元素上 for (; index < length; index++) { animation.tweens[index].run(percent); } // 触发notify回调列表 deferred.notifyWith(elem, [animation, percent, remaining]); // 如果执行进度为完成且tweens数组有元素 // 返回剩余时间 if (percent < 1 && length) { return remaining; } else { // 否则表示已完成,触发resolve回调列表, // 并返回false值 deferred.resolveWith(elem, [animation]); return false; } }, animation = deferred.promise({ // 动画元素 elem: elem, // 需要动画的属性 props: jQuery.extend({}, properties), // 给optall添加specialEasing属性对象 opts: jQuery.extend(true, { specialEasing: {} }, options), // 原始动画属性 originalProperties: properties, // 原始的配置项optall originalOptions: options, // 动画开始时间,使用当前时间的毫秒数 startTime: fxNow || createFxNow(), // 动画时长 duration: options.duration, tweens: [], 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; }, stop: function(gotoEnd) { var index = 0, // if we are going to the end, we want to run all the tweens // otherwise we skip this part length = gotoEnd ? animation.tweens.length : 0; if (stopped) { return this; } stopped = true; for (; index < length; index++) { animation.tweens[index].run(1); } // resolve when we played the last frame // otherwise, reject if (gotoEnd) { deferred.resolveWith(elem, [animation, gotoEnd]); } else { deferred.rejectWith(elem, [animation, gotoEnd]); } return this; } }), props = animation.props; /* 将是动画属性转换成驼峰式,并设置其相应的缓动属性, 如果存在cssHooks钩子对象,则需要另作一番处理 */ propFilter(props, animation.opts.specialEasing); // 遍历动画预过滤器,并执行回调 // 其中defaultPrefilter为默认预过滤器,每次都会执行 for (; index < length; index++) { result = animationPrefilters[index].call(animation, elem, props, animation.opts); // 如果有返回值,退出函数 if (result) { return result; } } createTweens(animation, props); if (jQuery.isFunction(animation.opts.start)) { animation.opts.start.call(elem, animation); } // 开始执行动画 jQuery.fx.timer( jQuery.extend(tick, { elem: elem, anim: animation, queue: animation.opts.queue })); // attach callbacks from options return animation.progress(animation.opts.progress).done(animation.opts.done, animation.opts.complete).fail(animation.opts.fail).always(animation.opts.always); } /** * 动画属性调整与过滤 * * 将是动画属性转换成驼峰式,并设置其相应的缓动属性, * 如果存在cssHooks钩子对象,则需要另作一番处理 * @param {[type]} props [需要动画的属性] * @param {[type]} specialEasing [description] * @return {[type]} [description] */ function propFilter(props, specialEasing) { var value, name, index, easing, hooks; // camelCase, specialEasing and expand cssHook pass for (index in props) { // 驼峰化属性 name = jQuery.camelCase(index); // TODO easing = specialEasing[name]; // 属性值 value = props[index]; // 如果属性值是数组 if (jQuery.isArray(value)) { easing = value[1]; // 取数组第一个元素为属性值 value = props[index] = value[0]; } // 如果属性名精过驼峰化后,删除原有的属性名,减少占用内存 if (index !== name) { props[name] = value; delete props[index]; } // 处理兼容性的钩子对象 hooks = jQuery.cssHooks[name]; // 如果存在钩子对象且有expand属性 if (hooks && "expand" in hooks) { // 返回expand处理后的value值 // 该类型是一个对象,属性是 // (margin|padding|borderWidth)(Top|Right|Bottom|Left) value = hooks.expand(value); // 我们已经不需要name属性了 delete props[name]; // not quite $.extend, this wont overwrite keys already present. // also - reusing 'index' from above because we have the correct "name" for (index in value) { // 如果props没有(margin|padding|borderWidth)(Top|Right|Bottom|Left)属性 // 添加该属性和对应的值,并设置缓动属性 if (!(index in props)) { props[index] = value[index]; specialEasing[index] = easing; } } } else { // 没有钩子对象就直接设置其为缓动属性 specialEasing[name] = easing; } } } jQuery.Animation = jQuery.extend(Animation, { tweener: function(props, callback) { if (jQuery.isFunction(props)) { callback = props; props = ["*"]; } else { props = props.split(" "); } var prop, index = 0, length = props.length; for (; index < length; index++) { prop = props[index]; tweeners[prop] = tweeners[prop] || []; tweeners[prop].unshift(callback); } }, // 为animationPrefilters回调数组添加回调 prefilter: function(callback, prepend) { if (prepend) { animationPrefilters.unshift(callback); } else { animationPrefilters.push(callback); } } }); /** * 动画预处理 * 添加fx队列缓存(没有的话),对动画属性“width/height,overflow”, 值有“toggle/show/hide”采取的一些措施 * * @param {[type]} elem [动画元素] * @param {[type]} props [动画属性] * @param {[type]} opts [动画配置项] * @return {[type]} [description] */ function defaultPrefilter(elem, props, opts) { /*jshint validthis:true */ var prop, index, length, value, dataShow, toggle, tween, hooks, oldfire, // animation对象(同时是个deferred对象) anim = this, style = elem.style, orig = {}, handled = [], hidden = elem.nodeType && isHidden(elem); // handle queue: false promises if (!opts.queue) { // 获取或者设置动画队列钩子 hooks = jQuery._queueHooks(elem, "fx"); // 如果hooks.unqueued为null/undefined if (hooks.unqueued == null) { hooks.unqueued = 0; // 获取旧的empty回调对象 // 用于清除动画队列缓存 oldfire = hooks.empty.fire; // 装饰,添加新的职责 hooks.empty.fire = function() { // 当hooks.unqueued为0时执行清除动画队列缓存 if (!hooks.unqueued) { oldfire(); } }; } hooks.unqueued++; anim.always(function() { // doing this makes sure that the complete handler will be called // before this completes // 延迟处理,确保该回调完成才调用下面回调 anim.always(function() { hooks.unqueued--; // 如果动画队列没有元素了,清空缓存 if (!jQuery.queue(elem, "fx").length) { hooks.empty.fire(); } }); }); } // height/width overflow pass // 对width或height的DOM元素的动画前的处理 if (elem.nodeType === 1 && ("height" in props || "width" in props)) { // Make sure that nothing sneaks out // Record all 3 overflow attributes because IE does not // change the overflow attribute when overflowX and // overflowY are set to the same value // IE不会改变overflow属性当iverflowX和overflowY的值相同时。 // 因此我们要记录三个overflow的属性 opts.overflow = [style.overflow, style.overflowX, style.overflowY]; // Set display property to inline-block for height/width // animations on inline elements that are having width/height animated // 将inline元素(非浮动的)设置为inline-block或者BFC(iE6/7),使它们的width和height可改变 if (jQuery.css(elem, "display") === "inline" && jQuery.css(elem, "float") === "none") { // inline-level elements accept inline-block; // block-level elements need to be inline with layout if (!jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay(elem.nodeName) === "inline") { style.display = "inline-block"; } else { style.zoom = 1; } } } if (opts.overflow) { style.overflow = "hidden"; // 如果不支持父元素随着子元素宽度改变而改变 // 动画结束后将style设置为初始状态 if (!jQuery.support.shrinkWrapBlocks) { anim.always(function() { style.overflow = opts.overflow[0]; style.overflowX = opts.overflow[1]; style.overflowY = opts.overflow[2]; }); } } // show/hide pass // 遍历动画属性 for (index in props) { // 获取目标值 value = props[index]; // 判断值是否有toggle|show|hide if (rfxtypes.exec(value)) { delete props[index]; // 是否需要toggle toggle = toggle || value === "toggle"; // 如果hide(或者show)状态的初始值和我们动画的值相同,就不需要做处理 if (value === (hidden ? "hide" : "show")) { continue; } // 将需要show/hide/toggle的属性保存到handled数组中 handled.push(index); } } length = handled.length; // 如果handled数组有元素 // 对需要toggle|show|hide的属性处理 if (length) { // 获取或者设置元素的fxshow缓存(保存显示状态) dataShow = jQuery._data(elem, "fxshow") || jQuery._data(elem, "fxshow", {}); // 如果元素已经有hidden属性,说明我们设置过了, // 取该值 if ("hidden" in dataShow) { hidden = dataShow.hidden; } // store state if its toggle - enables .stop().toggle() to "reverse" // 如果需要toggle,将hidden状态取反 if (toggle) { dataShow.hidden = !hidden; } // 如果元素隐藏了就显示出来,为了后期的动画 if (hidden) { jQuery(elem).show(); } else { // 否则动画结束后才隐藏 anim.done(function() { jQuery(elem).hide(); }); } // 动画结束后删除fxshow缓存,并恢复元素原始样式 anim.done(function() { var prop; jQuery._removeData(elem, "fxshow"); for (prop in orig) { jQuery.style(elem, prop, orig[prop]); } }); for (index = 0; index < length; index++) { prop = handled[index]; // 创建Tween实例 tween = anim.createTween(prop, hidden ? dataShow[prop] : 0); // 获取元素原始样式值 orig[prop] = dataShow[prop] || jQuery.style(elem, prop); // 如果dataShow引用的缓存没有show|hide|toggle属性 if (!(prop in dataShow)) { // 添加该属性,并赋初值 dataShow[prop] = tween.start; if (hidden) { tween.end = tween.start; tween.start = prop === "width" || prop === "height" ? 1 : 0; } } } } } // 实例化init构造函数 // 对单个动画属性,在初始化的时候计算开始值 function Tween(elem, options, prop, end, easing) { return new Tween.prototype.init(elem, options, prop, end, easing); } jQuery.Tween = Tween; Tween.prototype = { constructor: Tween, init: function(elem, options, prop, end, easing, unit) { this.elem = elem; this.prop = prop; this.easing = easing || "swing"; this.options = options; this.start = this.now = this.cur(); this.end = end; this.unit = unit || (jQuery.cssNumber[prop] ? "" : "px"); }, cur: function() { var hooks = Tween.propHooks[this.prop]; return hooks && hooks.get ? hooks.get(this) : Tween.propHooks._default.get(this); }, // 通过缓动算法计算出样式值,然后应用到元素上 run: function(percent) { var eased, hooks = Tween.propHooks[this.prop]; // 当前执行位置, // 如果有时长,就用缓动算法 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); } // 有钩子对象就执行set方法,否则使用默认set方法 if (hooks && hooks.set) { hooks.set(this); } else { Tween.propHooks._default.set(this); } return this; } }; Tween.prototype.init.prototype = Tween.prototype; Tween.propHooks = { _default: { // 默认的获取样式初始值方法 get: function(tween) { var result; if (tween.elem[tween.prop] != null && (!tween.elem.style || tween.elem.style[tween.prop] == null)) { return tween.elem[tween.prop]; } // passing an empty string as a 3rd parameter to .css will automatically // attempt a parseFloat and fallback to a string if the parse fails // so, simple values such as "10px" are parsed to Float. // complex values such as "rotate(1rad)" are returned as is. result = jQuery.css(tween.elem, tween.prop, ""); // Empty strings, null, undefined and "auto" are converted to 0. return !result || result === "auto" ? 0 : result; }, // 设置元素样式 set: function(tween) { // use step hook for back compat - use cssHook if its there - use .style if its // available and use plain properties where available if (jQuery.fx.step[tween.prop]) { jQuery.fx.step[tween.prop](tween); } else if (tween.elem.style && (tween.elem.style[jQuery.cssProps[tween.prop]] != null || jQuery.cssHooks[tween.prop])) { jQuery.style(tween.elem, tween.prop, tween.now + tween.unit); } else { tween.elem[tween.prop] = tween.now; } } } }; // Remove in 2.0 - this supports IE8's panic based approach // to setting things on disconnected nodes Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { set: function(tween) { if (tween.elem.nodeType && tween.elem.parentNode) { tween.elem[tween.prop] = tween.now; } } }; jQuery.each(["toggle", "show", "hide"], function(i, name) { var cssFn = jQuery.fn[name]; jQuery.fn[name] = function(speed, easing, callback) { return speed == null || typeof speed === "boolean" ? cssFn.apply(this, arguments) : this.animate(genFx(name, true), speed, easing, callback); }; }); jQuery.fn.extend({ fadeTo: function(speed, to, easing, callback) { // show any hidden elements after setting opacity to 0 return this.filter(isHidden).css("opacity", 0).show() // animate to the value specified .end().animate({ opacity: to }, speed, easing, callback); }, animate: function(prop, speed, easing, callback) { var // prop对象是否为空 empty = jQuery.isEmptyObject(prop), // 返回{complete, duration, easing, queue, old} optall = jQuery.speed(speed, easing, callback), // TODO doAnimation = function() { // Operate on a copy of prop so per-property easing won't be lost var anim = Animation(this, jQuery.extend({}, prop), optall); doAnimation.finish = function() { anim.stop(true); }; // Empty animations, or finishing resolves immediately if (empty || jQuery._data(this, "finish")) { anim.stop(true); } }; doAnimation.finish = doAnimation; // 如果prop为空对象或者queue为false(即不进行动画队列), // 遍历元素集并执行doAnimation回调 return empty || optall.queue === false ? this.each(doAnimation) : // 否则prop不为空且需要队列执行, // 将doAnimation添加到该元素的队列中 // jQuery.queue('fx', doAnimation) this.queue(optall.queue, doAnimation); }, // 停止所有在指定元素上正在运行的动画。 stop: function(type, clearQueue, gotoEnd) { var stopQueue = function(hooks) { var stop = hooks.stop; delete hooks.stop; stop(gotoEnd); }; if (typeof type !== "string") { gotoEnd = clearQueue; clearQueue = type; type = undefined; } if (clearQueue && type !== false) { this.queue(type || "fx", []); } return this.each(function() { var dequeue = true, index = type != null && type + "queueHooks", timers = jQuery.timers, data = jQuery._data(this); if (index) { if (data[index] && data[index].stop) { stopQueue(data[index]); } } else { for (index in data) { if (data[index] && data[index].stop && rrun.test(index)) { stopQueue(data[index]); } } } for (index = timers.length; index--;) { if (timers[index].elem === this && (type == null || timers[index].queue === type)) { timers[index].anim.stop(gotoEnd); dequeue = false; timers.splice(index, 1); } } // start the next in the queue if the last step wasn't forced // timers currently will call their complete callbacks, which will dequeue // but only if they were gotoEnd if (dequeue || !gotoEnd) { jQuery.dequeue(this, type); } }); }, finish: function(type) { if (type !== false) { type = type || "fx"; } return this.each(function() { var index, data = jQuery._data(this), queue = data[type + "queue"], hooks = data[type + "queueHooks"], timers = jQuery.timers, length = queue ? queue.length : 0; // enable finishing flag on private data data.finish = true; // empty the queue first jQuery.queue(this, type, []); if (hooks && hooks.cur && hooks.cur.finish) { hooks.cur.finish.call(this); } // look for any active animations, and finish them for (index = timers.length; index--;) { if (timers[index].elem === this && timers[index].queue === type) { timers[index].anim.stop(true); timers.splice(index, 1); } } // look for any animations in the old queue and finish them for (index = 0; index < length; index++) { if (queue[index] && queue[index].finish) { queue[index].finish.call(this); } } // turn off finishing flag delete data.finish; }); } }); // Generate parameters to create a standard animation /** * 用于填充slideDown/slideUp/slideToggle动画参数 * @param {[String]} type [show/hide/toggle] * @param {[type]} includeWidth [是否需要包含宽度] * @return {[type]} [description] */ function genFx(type, includeWidth) { var which, attrs = { height: type }, i = 0; // if we include width, step value is 1 to do all cssExpand values, // if we don't include width, step value is 2 to skip over Left and Right includeWidth = includeWidth ? 1 : 0; // 不包含宽度,which就取“Top/Bottom”, // 否则“Left/Right” for (; i < 4; i += 2 - includeWidth) { which = cssExpand[i]; attrs["margin" + which] = attrs["padding" + which] = type; } if (includeWidth) { attrs.opacity = attrs.width = type; } return attrs; } // Generate shortcuts for custom animations jQuery.each({ slideDown: genFx("show"), slideUp: genFx("hide"), slideToggle: genFx("toggle"), fadeIn: { opacity: "show" }, fadeOut: { opacity: "hide" }, fadeToggle: { opacity: "toggle" } }, function(name, props) { jQuery.fn[name] = function(speed, easing, callback) { return this.animate(props, speed, easing, callback); }; }); /** * 配置动画参数 * * 配置动画时长,动画结束回调(经装饰了),缓动算法,queue属性用来标识是动画队列 * @param {[Number|Objecct]} speed [动画时长] * @param {[Function]} easing [缓动算法] * @param {Function} fn [动画结束会掉] * @return {[Object]} [description] */ jQuery.speed = function(speed, easing, fn) { var opt = // speed是否为对象 speed && typeof speed === "object" ? // 如果是,克隆speed对象 jQuery.extend({}, speed) : // 否则返回一个新的对象 { // complete是我们的animate的回调方法, // 即动画结束时的回调 // (speed, easing, fn) // (speed || easing, fn) // (fn) complete: fn || !fn && easing || jQuery.isFunction(speed) && speed, // 动画时长 duration: speed, // 缓动 easing: fn && easing || easing && !jQuery.isFunction(easing) && easing }; opt.duration = // jQuery.fx.off是否为真,如果是则将opt.duration设置为0, // 这将会停止所有动画 jQuery.fx.off ? 0 : // 否则判断duration属性值是否为数字类型,是则使用 typeof opt.duration === "number" ? opt.duration : // 否则判断duration属性值字符串是否在jQuery.fx.speeds(jQuery的预配置动画时长)属性key字段中,是则使用 opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : // 否则就是用默认动画时长 jQuery.fx.speeds._default; // normalize opt.queue - true/undefined/null -> "fx" // 如果opt.queue的值是true/undefined/null之一, // 将其值设置为"fx"字符串,标示动画队列 if (opt.queue == null || opt.queue === true) { opt.queue = "fx"; } // Queueing // 将旧的回调(即我们添加的回调)存入opt.old opt.old = opt.complete; // 给opt.complete重新定义, // 在旧方法中通过装饰包装 opt.complete = function() { if (jQuery.isFunction(opt.old)) { // 执行我们的回调 opt.old.call(this); } // 如果有队列,执行我们下一个队列 if (opt.queue) { jQuery.dequeue(this, opt.queue); } }; // 返回opt /* {complete, duration, easing, queue, old} */ return opt; }; jQuery.easing = { linear: function(p) { return p; }, swing: function(p) { return 0.5 - Math.cos(p * Math.PI) / 2; } }; // 全局timers数组,保存着所有动画tick jQuery.timers = []; jQuery.fx = Tween.prototype.init; // setInterval回调 jQuery.fx.tick = function() { var timer, timers = jQuery.timers, i = 0; fxNow = jQuery.now(); // 遍历所有tick for (; i < timers.length; i++) { timer = timers[i]; // Checks the timer has not already been removed // 如果当前tick返回的为假(经弱转换) // 移除该tick // 然后继续遍历当前项,因为数组长度被改变了 if (!timer() && timers[i] === timer) { timers.splice(i--, 1); } } // 如果没有tick回调了,停止定时器 if (!timers.length) { jQuery.fx.stop(); } fxNow = undefined; }; /** * * * @param {Object} timer tick回调 */ jQuery.fx.timer = function(timer) { if (timer() && jQuery.timers.push(timer)) { jQuery.fx.start(); } }; jQuery.fx.interval = 13; // 动画正式开始 jQuery.fx.start = function() { if (!timerId) { // 间隔执行jQuery.fx.tick timerId = setInterval(jQuery.fx.tick, jQuery.fx.interval); } }; jQuery.fx.stop = function() { clearInterval(timerId); timerId = null; }; jQuery.fx.speeds = { slow: 600, fast: 200, // Default speed _default: 400 }; // Back Compat jQuery.fx.step = {}; if (jQuery.expr && jQuery.expr.filters) { jQuery.expr.filters.animated = function(elem) { return jQuery.grep(jQuery.timers, function(fn) { return elem === fn.elem; }).length; }; }