Animations under single threaded JavaScript
Asked Answered
C

4

9

JavaScript is a single threaded language and therefore it executes one command at a time. Asynchronous programming is being implemented via Web APIs (DOM for event handling, XMLHttpRequest for AJAX calls, WindowTimers for setTimeout) and the Event queue which are managed by the browser. So far, so good! Consider now, the following very simple code:

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');
... 

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?

If your answer is that animation creates promises, the question remains the same: How JavaScript is dealing with more than one thing at the same time (executing the animation itself, watching the animation queue in case of promises and proceeding with the code that follows...).

Moreover, I cannot explain how promises in jQuery work if they have to watch their parent Deferred object till it is resolved or rejected that means code execution and at the same time the remaining code is executed. How is that possible in a single threaded approach? I have no problem to understand AJAX calls for I know they are taken away from JS engine...

Corespondent answered 12/2, 2016 at 12:16 Comment(7)
I'm not sure what I will say right now, but to me it's seems than Js just register your hide action. Once is done, it's keep going to the next step, displaying your console.log, witch is registered before the hide on the register stack. Then, when js hit your hide on his stack, it will begin to start the transition effect. Dunno if i'm clear, if not tell me you could maybe find a way to show you thatCambridgeshire
Many modern js animation routines use the requestAnimationFrame() function. This allows for smoother animations and is non blocking.Dysentery
@Dysentery jQuery doesn't use requestAnimationFrame because it has surprising behavior on inactive tabs. Then again so do timers.Feathering
@BenjaminGruenbaum jQuery 3.0 uses requestAnimationFrame , see blog.jquery.com/2015/07/13/…Lavinalavine
@Lavinalavine thanks, I stand corrected.Feathering
If you're interested in general about the inner workings of the event loop, check out latentflip.com/loupeIssuable
Philip Roberts has a great presentation on the topic: youtube.com/watch?v=8aGhZQkoFbQ.Alms
F
21

tl;dr; it would not be possible in a strictly single threaded environment without outside help.


I think I understand your issue. Let's get a few things out of the way:

JavaScript is always synchronous

No asynchronous APIs are defined in the language specification. All the functions like Array.prototype.map or String.fromCharCode always run synchronously*.

Code will always run to completion. Code does not stop running until it is terminated by a return, an implicit return (reaching the end of the code) or a throw (abruptly).

a();
b();
c();
d(); // the order of these functions executed is always a, b, c, d and nothing else will 
     // happen until all of them finish executing

JavaScript lives inside a platform

The JavaScript language defines a concept called a host environment:

In this way, the existing system is said to provide a host environment of objects and facilities, which completes the capabilities of the scripting language.

The host environment in which JavaScript is run in the browser is called the DOM or document object model. It specifies how your browser window interacts with the JavaScript language. In NodeJS for example the host environment is entirely different.

While all JavaScript objects and functions run synchronously to completion - the host environment may expose functions of its own which are not necessarily defined in JavaScript. They do not have the same restrictions standard JavaScript code has and may define different behaviors - for example the result of document.getElementsByClassName is a live DOM NodeList which has very different behavior from your ordinary JavaScript code:

var els = document.getElementsByClassName("foo"); 
var n = document.createElement("div");
n.className = "foo";
document.body.appendChild(n);
els.length; // this increased in 1, it keeps track of the elements on the page
            // it behaves differently from a JavaScript array for example. 

Some of these host functions have to perform I/O operations like schedule timers, perform network requests or perform file access. These APIs like all the other APIs have to run to completion. These APIs are by the host platform - they invoke capabilities your code doesn't have - typically (but not necessarily) they're written in C++ and use threading and operating system facilities for running things concurrently and in parallel. This concurrency can be just background work (like scheduling a timer) or actual parallelism (like WebWorkers - again part of the DOM and not JavaScript).

So, when you invoke actions on the DOM like setTimeout, or applying a class that causes CSS animation it is not bound to the same requirements your code has. It can use threading or operating system async io.

When you do something like:

setTimeout(function() {
   console.log("World");
});
console.log("Hello");

What actually happens is:

  • The host function setTimeout is called with a parameter of type function. It pushes the function into a queue in the host environment.
  • the console.log("Hello") is executed synchronously.
  • All other synchronous code is run (note, the setTimeout call was completely synchronous here).
  • JavaScript finished running - control is transferred to the host environment.
  • The host environment notices it has something in the timers queue and enough time has passed so it calls its argument (the function) - console.log("World") is executed.
  • All other code in the function is run synchronously.
  • Control is yielded back to the host environment (platform).
  • Something else happens in the host environment (mouse click, AJAX request returning, timer firing). The host environment calls the handler the user passed to these actions.
  • Again all JavaScript is run synchronously.
  • And so on and so on...

Your specific case

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');

Here the code is run synchronously. The previous command has terminated, but it did not actually do much - instead it scheduled a callback on the platform a(in the .hide(17000) and then executed the console.log since again - all JavaScirpt code runs synchronously always.

That is - hide performs very little work and runs for a few milliseconds and then schedules more work to be done later. It does not run for 17 seconds.

Now the implementation of hide looks something like:

function hide(element, howLong) {
    var o = 16 / howLong; // calculate how much opacity to reduce each time
    //  ask the host environment to call us every 16ms
    var t = setInterval(function
        // make the element a little more transparent
        element.style.opacity = (parseInt(element.style.opacity) || 1) - o;
        if(parseInt(element.style.opacity) < o) { // last step
           clearInterval(t); // ask the platform to stop calling us
           o.style.display = "none"; // mark the element as hidden
        }
    ,16);
}

So basically our code is single threaded - it asks the platform to call it 60 times a second and makes the element a little less visible each time. Everything is always run to completion but except for the first code execution the platform code (the host environment) is calling our code except for vice versa.

So the actual straightforward answer to your question is that the timing of the computation is "taken away" from your code much like in when you make an AJAX request. To answer it directly:

It would not be possible in a single threaded environment without help from outside.

That outside is the enclosing system that uses either threads or operating system asynchronous facilities - our host environment. It could not be done without it in pure standard ECMAScript.

* With the ES2015 inclusion of promises, the language delegates tasks back to the platform (host environment) - but that's an exception.

Feathering answered 18/2, 2016 at 23:14 Comment(5)
Where can I see what functions/api the host environment provides?Apiary
@Imray dom.spec.whatwg.org for the browser - for example MutationObserver. There are implementations of the DOM host environment for other languages too by the way php.net/manual/en/book.dom.php . For NodeJS nodejs.org/api for example the file system nodejs.org/api/fs.html . All these, and functions built using them can interact with the host environment.Feathering
@Benjamin thanks very much for your answer. But, if we change the second line(console.log) with a stop timer e.g var now=new Date().getTime(); var stop=now+17000; while (stop> new Date().getTime()){} what should we expect according to your analysis? Execute first the 17 seconds waiting and then dealing with the event queue and the animation callback inside it. However, what we get is animation finishes simultaneously with the stop timer; that means did not go to the event queue. So, what happened? And something else, where did you find the code for function hide()?Corespondent
@ilias I think you're being confused by the same thing that confuses you in my answer. JQuery uses interpolation (rather than dividing the animation equally), so it gives the impression that the .hide animation had been running in the background, but it hasn't.Cytolysis
@ILIAS JavaScript code always runs synchronously. If we first execute the 17 seconds busy wait - then the screen will freeze for 17 seconds since nothing else can happen since JavaScript always run synchronously to completion.Feathering
I
2

You have several kind of functions in javascript: Blocking and non blocking.

Non blocking function will return immediately and the event loop continues execution while it work in background waiting to call the callback function (like Ajax promises).

Animation relies on setInterval and/or setTimeout and these two methods return immediately allowing code to resume. The callback is pushed back into the event loop stack, executed, and the main loop continues.

Hope this'll help.

You can have more information here or here

Ibadan answered 15/2, 2016 at 13:6 Comment(0)
C
0

Event Loop

JavaScript uses what is called an event loop. The event loop is like a while(true) loop.

To simplify it, assume that JavaScript has one gigantic array where it stores all the events. The event loop loops through this event loop, starting from the oldest event to the newest event. That is, JavaScript does something like this:

while (true) {
     var event = eventsArray.unshift();

     if (event) {
       event.process();
     }
}

If, during the processing of the event (event.process), a new event is fired (let's call this eventA), the new event is saved in the eventsArray and execution of the current continues. When the current event is done processing, the next event is processed and so on, until we reach eventA.

Coming to your sample code,

$('#mybox').hide(17000);
console.log('Previous command has not yet terminated!');

When the first line is executed, an event listener is created and a timer is started. Say jQuery uses 100ms frames. A timer of 100ms is created, with a callback function. The timer starts running in the background (the implementation of this is internal to the browser), while the control is given back to your script. So, while the timer is running in the background, your script continues to line two. After 100ms, the timer finishes, and fires an event. This event is saved in the eventsArray above, it does not get executed immediately. Once your code is done executing, JavaScript checks the eventsArray and sees that there is one new event, and then executes it.

The event is then run, and your div or whatever element it is moves a few pixels, and a new 100ms timer starts.

Please note that this is a simplification, not the actual working of the whole thing. There are a few complications to the whole thing, like the stack and all. Please see the MDN article here for more info.

Cytolysis answered 18/2, 2016 at 13:26 Comment(11)
I did not ask for Event loop explanation about which I know enough. I am sure you did not read my question carefully. Moreover, your answer is not correct. If animation 'd be implemented the way you described it 'd be frozen till the whole script execution!! However, it is very easy to remark that the animation continues to run at the same time the remaining code is being executed. That is not compatible with your answer and that's exactly what am I looking for to explain...Corespondent
The jQuery animation is not implemented exactly that way (I am sure they use interval, instead of timeout, with some time-based interpolations to make the animation smoother), but I am not sure why you think the answer is incorrect. ...continued belowCytolysis
And, no, the animation does not continue to run at the same time as the code is being executed. Contrary to your "understanding", the animation actually waits for the remaining code to finish before continuing. It all just happens too fast for you to notice. As an experiment, you can run the following code to see what happens: $('#mybox').hide(17000); var start = new Date(); while(true) { var now = new Date(); if (now-start > 20000) { break; } }Cytolysis
The code above basically makes the code after the $('#mybox').hide(17000); to run slower, thereby slowing down the execution of the next event in the event loop.Cytolysis
what you wrote is exactly what I mean! The .hide() animation will run during the execution of while loop (it is very easy to check it by giving some dimensions and color to mydiv) and that is what I cannot explain...Corespondent
Sorry my friend, you're misunderstanding the whole thing. The .hide() does not run during the execution of the while loop. If you ran the code above, you'll notice that the animation does not run. After about 20 seconds, the div just disappears without any animation. Without the while loop, the div doesn't just disappear, it smoothly fades away and shrinks in size until it's gone. What is actually happening is that jQuery uses some nice trick. ...continuedCytolysis
It saves the time the .hide() started the animation (the animation is initialized before the rest of the code continues). When the rest of the code finishes running, and it's time to run the next frame in the event loop (which we assume is the .loop's next tick), jQuery compares the current time with the time the animation started, and finds out that more than 17 seconds have already passed, so it just removes the item from the screen without trying to animate it anymore. ...continuedCytolysis
If 12 seconds had passed for example, it would jump the animation to where it would be after 12 seconds if there were no delays. I think you're either being confused by the little trickery or we're not understanding each other.Cytolysis
Your explanation is reasonable and partially it gives an answer to my query. However, are you sure that animation is not running in the background and we see its current state at the moment the while loop breaks? According to your approach, animation does not start and just begins at the point it should be if there was no while loop. Once again are you sure 100% about that? Does jQuery code support your approach? Personally, I cannot find(inside jQuery code) any clue of sending animations to the Event queue. How is this implemented? What do you think?Corespondent
Let us continue this discussion in chat.Corespondent
I have not looked at the jQuery code that implements animations. However, from the behaviour, I can say I am 99.9% certain what they do. I have worked with many JS animation libraries (including 3D libs), and this pattern is quite common. The truth is that JS can't run two codes at a time, no matter how you look at it.Cytolysis
L
0

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>
Lavinalavine answered 19/2, 2016 at 0:29 Comment(1)
This is all technically correct but completely irrelevant to the actual question OP asked.Feathering

© 2022 - 2024 — McMap. All rights reserved.