Could someone please explain to me the underlying mechanism of the
above? Since .hide() has not yet finished (the animation lasts 17
seconds) and JS engine is dealing with it and it is capable of
executing one command at a time, in which way does it go to the next
line and continues to run the remaining code?
jQuery.fn.hide()
internally calls jQuery.fn.animate
which calls jQuery.Animation
which returns a jQuery deferred.promise()
object; see also jQuery.Deferred()
The deferred.promise()
method allows an asynchronous function to
prevent other code from interfering with the progress or status of its
internal request.
For description of Promise
see Promises/A+ , promises-unwrapping , Basic Javascript promise implementation attempt ; also , What is Node.js?
jQuery.fn.hide
:
function (speed, easing, callback) {
return speed == null || typeof speed === "boolean"
? cssFn.apply(this, arguments)
: this.animate(genFx(name, true), speed, easing, callback);
}
jQuery.fn.animate
:
function animate(prop, speed, easing, callback) {
var empty = jQuery.isEmptyObject(prop),
optall = jQuery.speed(speed, easing, callback),
doAnimation = function () {
// Operate on a copy of prop so per-property easing won't be lost
var anim = Animation(this, jQuery.extend({},
prop), optall);
// Empty animations, or finishing resolves immediately
if (empty || jQuery._data(this, "finish")) {
anim.stop(true);
}
};
doAnimation.finish = doAnimation;
return empty || optall.queue === false ? this.each(doAnimation) : this.queue(optall.queue, doAnimation);
}
jQuery.Animation
:
function Animation(elem, properties, options) {
var result, stopped, index = 0,
length = animationPrefilters.length,
deferred = jQuery.Deferred().always(function () {
// don't match elem in the :animated selector
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,
length = animation.tweens.length;
for (; index < length; index++) {
animation.tweens[index].run(percent);
}
deferred.notifyWith(elem, [animation, percent, remaining]);
if (percent < 1 && length) {
return remaining;
} else {
deferred.resolveWith(elem, [animation]);
return false;
}
},
animation = deferred.promise({
elem: elem,
props: jQuery.extend({},
properties),
opts: jQuery.extend(true, {
specialEasing: {}
},
options),
originalProperties: properties,
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;
propFilter(props, animation.opts.specialEasing);
for (; index < length; index++) {
result = animationPrefilters[index].call(animation, elem, props, animation.opts);
if (result) {
return result;
}
}
jQuery.map(props, createTween, animation);
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);
}
When .hide()
is called , a jQuery.Deferred()
is created that processes the animation tasks.
This is the reason console.log()
is called.
If include start
option of .hide()
can review that .hide()
begins before console.log()
is called on next line, though does not block the user interface from performing asynchronous tasks.
$("#mybox").hide({
duration:17000,
start:function() {
console.log("start function of .hide()");
}
});
console.log("Previous command has not yet terminated!");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<div id="mybox">mybox</div>
Native Promise
implementation
function init() {
function $(id) {
return document.getElementById(id.slice(1))
}
function hide(duration, start) {
element = this;
var height = parseInt(window.getComputedStyle(element)
.getPropertyValue("height"));
console.log("hide() start, height", height);
var promise = new Promise(function(resolve, reject) {
var fx = height / duration;
var start = null;
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
height = height - fx * 20.5;
element.style.height = height + "px";
console.log(height, progress);
if (progress < duration || height > 0) {
window.requestAnimationFrame(step);
} else {
resolve(element);
}
}
window.requestAnimationFrame(step);
});
return promise.then(function(el) {
console.log("hide() end, height", height);
el.innerHTML = "animation complete";
return el
})
}
hide.call($("#mybox"), 17000);
console.log("Previous command has not yet terminated!");
}
window.addEventListener("load", init)
#mybox {
position: relative;
height:200px;
background: blue;
}
<div id="mybox"></div>
requestAnimationFrame
, see blog.jquery.com/2015/07/13/… – Lavinalavine