JavaScript single threaded animations

JavaScript is a single-threaded language, and therefore it executes one command at a time. Asynchronous programming is implemented through the Web API (DOM for event processing, XMLHttpRequest for AJAX calls, WindowTimers for setTimeout) and a queue of events that are controlled by the browser. So far, so good! Now consider the following very simple code:

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

Can someone explain to me the main mechanism of the above? Since .hide () is not finished yet (the animation lasts 17 seconds), and the JS engine deals with it, and it is able to execute one command at a time, and in which direction does it go to the next line and continue to run the remaining code?

If your answer is that the animation creates promises, the question remains the same : how does JavaScript work with several objects at the same time (execution of the animation itself, viewing the animation queue in case of promises and continuing with the following code ...)

Also, I can't explain how promises in jQuery work if they have to watch their Pending Parent until it is resolved or rejected , which means executing the code , and at the same time, the remaining code is executed. How is this possible in a single stream approach? I have no problem understanding AJAX calls, as I know they are taken from the JS engine ...

+7
javascript jquery-animate single-threaded
source share
4 answers

tl; dr; , this would not be possible in a strictly single-threaded environment without external assistance.


I think I understand your problem. Let me get some things out of the way:

JavaScript is always in sync

There are no asynchronous APIs in the language specification. All functions like Array.prototype.map or String.fromCharCode always work synchronously *.

The code will always work until completion. The code does not stop until it completes with return , an implicit return (to the end of the code), or throw (all of a sudden).

 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 the platform

JavaScript defines a concept called host environment :

Thus, the existing system is reported to provide a host environment for objects and objects that complements the scripting language.

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

Although all JavaScript objects and functions are executed synchronously until completion, the host environment can expose its own functions, which are not necessarily defined in JavaScript. They do not have the same limitations as standard JavaScript code and can define different types of behavior - for example, the result of document.getElementsByClassName is a live DOM NodeList that has very different behavior for your regular 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 must perform I / O, such as schedule timers, network requests, or file access. These APIs, like all other APIs, must be executed before completion. These APIs are located on the host platform - they call features that do not have your code, as a rule (but not necessarily), they are written in C ++ and use streaming and operating system tools for simultaneous and parallel operation. This concurrency can be just background work (e.g. scheduling a timer) or actual parallelism (e.g. WebWorkers - again part of the DOM, not JavaScript).

So, when you invoke actions in the DOM, such as setTimeout, or apply a class that invokes CSS animations, it is not tied to the same requirements as your code. It can use the async io streaming or operating system.

When you do something like:

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

What is actually going on:

  • The host function setTimeout is called with the parameter of the type function. It queues the function in the host environment .
  • console.log("Hello") runs synchronously.
  • All other synchronous code is executed (note that the setTimeout call was completely synchronous).
  • Finished launching JavaScript - control is transferred to the host environment.
  • The host environment notices that it has something in the timer queue and enough time has passed, so it calls its argument (function) - console.log("World") .
  • All other code in the function runs synchronously.
  • The control returns back to the host environment (platform).
  • Something else is happening in the host environment (mouse click, return AJAX request, enable timer). The host environment calls the handler that the user passed to these actions.
  • Again, all JavaScript runs synchronously.
  • And so on and so forth.

Your particular case

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

Here, the code runs synchronously. The previous command has completed, but in reality it is not so much - instead, it planned a callback on platform a (in .hide(17000) , and then executed console.log again) - all JavaScirpt code always runs synchronously.

That is, hide does very little work and works for a few milliseconds, and then plans more work to be done later. It does not work for 17 seconds.

The hide implementation now looks something like this:

 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); } 

Thus, basically our code is single-threaded - it asks the platform to call it 60 times per second and makes the element a little less noticeable every time. Everything is always executed until completion, but except for the first code execution, the platform code (host environment) calls our code, except vice versa.

So, the real direct answer to your question is that the computation time is "taken" from your code, as in the case of an AJAX request. To answer it directly:

This is not possible in a single streaming environment without outside help.

This external shell is a system that uses either threads or asynchronous operating system tools — our host environment. This could not have been done without it in the standard ECMAScript standard.

* With the inclusion of ES2015 promises, the language delegates tasks back to the platform (host environment), but this is an exception.

+18
source share

You have several functions in javascript: Lock and non-block.

A non-blocking function will immediately return, and the event loop will continue to execute while it is running in the background, waiting for a callback function to be called (for example, Ajax promises).

The animation is based on setInterval and / or setTimeout, and these two methods return immediately so that the code resumes. The callback is returned to the stack of the event loop, executed, and the main loop continues.

Hope this helps.

You may have more details here or here.

+2
source share

Event loop

JavaScript uses what is called an event loop . An event loop is similar to a while(true) .

To simplify this, suppose JavaScript has one gigantic array in which all events are stored. The cycle of events passes through this cycle of events, starting with the oldest event and ending with a new event. That is, JavaScript does something like this:

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

If during the processing of an event ( event.process ) a new event is eventA (let me call it eventA ), the new event is stored in eventsArray and the execution of the current one continues. When the current event completes processing, the next event is processed, and so on, until we reach eventA .

Getting started with your sample code,

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

When the first line is executed, the event listener starts and the timer starts. Let's say jQuery uses 100 ms frames. A 100 ms timer is created with a callback function. The timer starts in the background (the implementation of this is internal to the browser), and the control returns to your script. So, while the timer is running in the background, your script continues line two. After 100 ms, the timer ends and triggers the event. This event is stored in eventsArray above; it is not executed immediately. After executing your code, JavaScript checks for eventsArray and sees that there is one new event, and then executes it.

Then an event is executed, and your div or any other element moves a few pixels, and a new 100 ms timer begins.

Please note that this is a simplification, not an actual work of everything. There are several complications for everything, like a stack and that’s it. See the MDN Article here for more details.

0
source share

Can someone please explain to me the main mechanism above? Since .hide () is not finished yet (the animation lasts 17 seconds), and the JS engine deals with it, and it is able to execute one command at a time, and in which direction does it go to the next and continue to execute the remaining code?

jQuery.fn.hide() internally calls jQuery.fn.animate , which calls jQuery.Animation , which returns the 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 a description of Promise see Promises / A + , promises-unwrapping , Basic Attempt to Implement Javascript Promises ; 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, .hide() is created that handles the animation tasks.

It is for this reason that console.log() called.

If you enable the start parameter .hide() , you can verify that .hide() starts before console.log() is called on the next line, although it does not block the user 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> 
0
source share

All Articles