I have a block of code that executes when a button is clicked. The code uses a loop that sometimes takes a while to complete. When the user clicks the button, I want the cursor to change a "wait" cursor before the loop begins. Once the loop is finished, the cursor should return to normal.
What is actually happening (in Chrome for Windows at least) is that the style doesn't get updated until after the loop. It seems to be a quirk of how buttons work. I really don't know. I'm out of guesses!
A sample fiddle: http://jsfiddle.net/ra51npjr/1/ (it just uses console.log to execute "something"... you might need to change how many times the loop runs depending on how zippy or slow your machine is).
Sample HTML:
<div class="fakebody">
<button id="foo">Foo</button>
</div>
Sample CSS:
.fakeBody {
height: 1000px;
width: 100%;
}
.wait {
cursor: wait !important;
}
Sample JavaScript:
$('#foo').on('click', function (e) {
$('.fakebody').addClass('wait');
for (i = 0; i < 10000; i++) {
console.log(i);
}
$('.fakebody').removeClass('wait');
});
--
Here are my ASSUMPTIONS on how the script should work:
The click happens, which fires up the code. Indeed, if I log "started!" inside the code block, it will correctly log that it has started
The cursor should be a wait cursor so long as it is hovering anywhere over "fakebody".
The for loop is just a simple way to kill a few seconds to see the effect. Feel free to substitute any other loop that takes a while to complete
At the end of the loop, the cursor is no longer a wait cursor
What is actually happening:
The loop executes
At the end of the loop, the cursor turns to a "wait" cursor and then instantly back to a regular cursor. The change doesn't happen until the loop is complete
Does anybody know a technique or workaround to get the cursor to change before the loop starts instead of only after it is finished? Is this known behaviour that I need to educate myself about (and if so, do you know where I should start looking?)
This is a common issue in JavaScript. This question may provide some deeper insight, but essentially the point is that synchronous JavaScript execution must finish before the browser can perform other actions (like updating the view).
Because .addClass, the for loop, and .removeClass all occur synchronously, the browser doesn't get a chance to redraw anything. A technique that is often used in these cases is to setTimeout with a timeout of 0, which essentially just "yields" control back to the browser.
$('.fakebody').addClass('wait');
setTimeout(function() {
for (i = 0; i < 10000; i++) {
console.log(i);
}
$('.fakebody').removeClass('wait');
}, 0);
If this is a common pattern, you could potentially extract it out to a function (which would also help improve readability) that wraps the async setTimeout. Here's a simple example:
/**
* Wraps a long-running JavaScript process in a setTimeout
* which yields to allow the browser to process events, e.g. redraw
*/
function yieldLongRunning(preFn, fn, postFn, ctx) {
if (arguments.length <= 2) {
ctx = fn; fn = preFn;
preFn = postFn = function() {};
}
preFn.call(ctx);
setTimeout(function() {
fn.call(ctx);
postFn.call(ctx);
}, 0);
}
And use it like so:
yieldLongRunning(function() {
$('.fakebody').addClass('wait');
},
function() {
for (i = 0; i < 10000; i++) {
console.log(i);
}
},
function() {
$('.fakebody').removeClass('wait');
});
As a side point, note that setTimeout(..., 0) simply queues the function in the browser's event loop, alongside other queued JavaScript functions, as well as other types of events (like redraws). Thus, no setTimeout call is guaranteed to run precisely at the given time - the timeout argument is simply a lower-bound (and, in fact, there is a minimum timeout of 4ms specified by HTML5 spec, which browsers use to prevent infinite timeout loops; you can still use 0, though, and the browser will add it to the event queue after the minimum delay).
I think you should try to force a redraw by hiding + showing the parent element.
Try this:
document.getElementById('fakebody').style.display = 'none';
document.getElementById('fakebody').style.display = 'block';
Before and after the loop (i.e. when you want the child element "foo" to refresh.
EDIT: Since you're using jquery you could do this:
$('#fakebody').hide().show(0);
Demo - Use queue & dequeue to construct an order of what should happen when in jQuery.
$('#foo').on('click', function (e) {
$('.fakebody').addClass('wait').queue(function(n) {
for (i = 0; i < 10000; i++) { console.log(i); }
}).removeClass('wait').dequeue();
});
Related
In the UI of an application that I've been developing, I have been trying to show a "loader" div before a specific (synchronous) processing starts and hide it after the processing has ended. However, the loader does not appear at all during the whole processing.
I found out that browsers try to optimise the rendering process, by eliminating needless reflows, where possible. There are some ways to manually trigger the reflow process (for cases like mine), but I haven't managed to make any of them work. You can find below a minimal example, where the calculation of offsetHeight is used to trigger reflow, but the TEST div is not appearing at all.
<html>
<head>
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
</head>
<body>
<div id="test" style="display:none; background-color:red; height:500px">TEST</div>
<script>
function reflow() {
var t = $("#test")[0].offsetHeight;
$(window).trigger('resize');
window.getComputedStyle($("#test")[0], null);
$("#test").focus();
}
$("#test").show();
reflow();
console.log("Processing started");
var sum = 0;
for(i = 0; i < 100000000; i++) {
sum += i;
}
console.log("Processing ended");
$("#test").hide();
<script>
</body>
</html>
Working JSFiddle also here
Some of the ways attempted to force reflow are the following (as you can see in the script):
offsetHeight's calculation of element
window resize
window.getComputedStyle()
focus of element
NOTE: I know it can be solved, by adding a timeout. However, I want to find a way to force reflow, since timeout is a "hack" and is not appropriate for this functionality.
Use setTimeout with a "0" parameter for the wait, this will force it to stop just long enough for your show/hide to work.
https://jsfiddle.net/o3y60e0L/3/
$("#test").show();
var t = $("#test")[0].offsetHeight;
console.log("Processing started");
setTimeout(function() {
var sum = 0;
for(i = 0; i < 1000000000; i++) {
sum += i;
}
console.log("Processing ended");
$("#test").hide();
}, 0);
It finally seems that even when you trigger a re-flow event manually, as shown in my code, this event is added to an events queue. Since there is only 1 javascript thread per window, these events are handled only when the executing code has finished.
Even though I could not find an official documentation of this behaviour, I could neither trigger a synchronous reflow (while some code is executing) with any of the proposed techniques. So, the only solution seems to be to add a small timeout, so that thread is idle for a small period and can process the reflow event, before starting executing the remaining code (as suggested by #Dan Smolinske).
In order to avoid perplexing different functionalities, one can create a new method executeWithLoader, as below:
function executeWithLoader(callback) {
$("#test").show();
setTimeout(function() {
callback();
$("#test").hide();
}, 50, this);
}
And call this function from the class the executes the processing in the following way:
function processing() {
console.log("Processing started");
var sum = 0;
for(i = 0; i < 100000000; i++) {
sum += i;
}
console.log("Processing ended");
}
executeWithLoader(processing);
I leave this as accepted answer for now, but if there is any future answer that describes a successful way to trigger re-flow, I will switch it.
I have the following code which demonstrates the difference in calling a long-running function directly from an event trigger, vs. using setTimeout().
Intended behavior:
When the first button is pressed, it appears pressed, the calculation runs for several seconds, then when the calculation finishes, the button appears depressed again and the second column changes from "not calculating yet" to "calculation done". (I won't elaborate on why that is supposed to happen; it's explained in linked answer.)
When the second button is pressed, the button depresses immediately; the second column immediately changes to "calculating..." text. When the calculation finishes several seconds later, the second column changes from "calculating..." to "calculation done".
What actually happens:
This works perfectly in Chrome (both buttons behave as expected)
This works perfectly in Internet Explorer 8
This does NOT work in Firefox (v.25) as-is. Specifically, the second button behaves 100% as the first one.
Changing the timeout in setTimeout() from 0 to 1 has no effect
Changing the timeout in setTimeout() from 0 to 500 works
Which leaves me with a big conundrum.
According to the whole reason behind why setTimeout() works whereas lack of one doesn't, the delay should have zero effect on how things work, since setTimeout()'s main purpose is to change the queuing order here, NOT to delay things.
So, why is it not working with delay 0 or 1 on Firefox, but works as expected with delay 500 (and works with any delay on Internet Explorer 8/Chrome)?
UPDATE: In addition to source code below, I also made a JSFiddle. But for some reason JSFiddle refuses to even load on my Internet Explorer 8, so for that testing, the code below is required.
UPDATE2: Someone raised the possibility of there being an issue with configuration setting dom.min_timeout_value in Firefox. I have edited it from 4 to 0, restarted the browser, and nothing was fixed. It still fails with a timeout of 0 or 1 and succeeds with 500.
Here is my source code - I simply saved it to a HTML file on C: drive and opened in all three browsers:
<html><body>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<table border=1>
<tr><td><button id='do'>Do long calc - bad status!</button></td>
<td><div id='status'>Not Calculating yet.</div></td></tr>
<tr><td><button id='do_ok'>Do long calc - good status!</button></td>
<td><div id='status_ok'>Not Calculating yet.</div></td></tr>
</table>
<script>
function long_running(status_div) {
var result = 0;
for (var i = 0; i < 1000; i++) {
for (var j = 0; j < 700; j++) {
for (var k = 0; k < 200; k++) {
result = result + i + j + k;
}
}
}
$(status_div).text('calclation done');
}
// Assign events to buttons
$('#do').on('click', function () {
$('#status').text('calculating....');
long_running('#status');
});
$('#do_ok').on('click', function () {
$('#status_ok').text('calculating....');
window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
</script>
</body></html>
To test, you will need to change the nested loop boundaries to 300/100/100 for Internet Explorer 8; or to 1000/1000/500 for Chrome, due to different sensitivity of "this JS is taking too long" error coupled with JavaScript engine speed.
There is a copy of the current (Jun 28, 2016) implementation of window.setTimeout() in Ubuntu.
As we can see, the timer gets inserted by this line of code:
nsAutoPtr<TimeoutInfo>* insertedInfo =
mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));
Then a few lines below you have an if() statement:
if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
...
The insertedInfo == mTimeouts.Elements() checks whether the timer that was just inserted already timed out. The following block does NOT execute the attached function, but the main loop will immediately notice that a timer timed out and thus it will skip the IDLE state (a yield of the CPU) that you are expecting.
This clearly (at least to me) explains the behavior you are experiencing. The rendering on the screen is another process (task/thread) and the CPU needs to be relinquished for that other process to get a chance to re-paint the screen. For that to happen, you need to wait long enough so your timer function does not get executed immediately and a yield happens.
As you've notice a pause of 500ms does the trick. You can probably use a smaller number, such as 50ms. Either way it is not going to guarantee that a yield happens, but chances are it will happen if the computer on which that code is running is not currently swamped (i.e. an anti-virus is not currently running full speed in the background...)
The complete SetTimeout() function from Firefox:
(location of the file in the source: dom/workers/WorkerPrivate.cpp)
int32_t
WorkerPrivate::SetTimeout(JSContext* aCx,
dom::Function* aHandler,
const nsAString& aStringHandler,
int32_t aTimeout,
const Sequence<JS::Value>& aArguments,
bool aIsInterval,
ErrorResult& aRv)
{
AssertIsOnWorkerThread();
const int32_t timerId = mNextTimeoutId++;
Status currentStatus;
{
MutexAutoLock lock(mMutex);
currentStatus = mStatus;
}
// It's a script bug if setTimeout/setInterval are called from a close handler
// so throw an exception.
if (currentStatus == Closing) {
JS_ReportError(aCx, "Cannot schedule timeouts from the close handler!");
}
// If the worker is trying to call setTimeout/setInterval and the parent
// thread has initiated the close process then just silently fail.
if (currentStatus >= Closing) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
nsAutoPtr<TimeoutInfo> newInfo(new TimeoutInfo());
newInfo->mIsInterval = aIsInterval;
newInfo->mId = timerId;
if (MOZ_UNLIKELY(timerId == INT32_MAX)) {
NS_WARNING("Timeout ids overflowed!");
mNextTimeoutId = 1;
}
// Take care of the main argument.
if (aHandler) {
newInfo->mTimeoutCallable = JS::ObjectValue(*aHandler->Callable());
}
else if (!aStringHandler.IsEmpty()) {
newInfo->mTimeoutString = aStringHandler;
}
else {
JS_ReportError(aCx, "Useless %s call (missing quotes around argument?)",
aIsInterval ? "setInterval" : "setTimeout");
return 0;
}
// See if any of the optional arguments were passed.
aTimeout = std::max(0, aTimeout);
newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout);
uint32_t argc = aArguments.Length();
if (argc && !newInfo->mTimeoutCallable.isUndefined()) {
nsTArray<JS::Heap<JS::Value>> extraArgVals(argc);
for (uint32_t index = 0; index < argc; index++) {
extraArgVals.AppendElement(aArguments[index]);
}
newInfo->mExtraArgVals.SwapElements(extraArgVals);
}
newInfo->mTargetTime = TimeStamp::Now() + newInfo->mInterval;
if (!newInfo->mTimeoutString.IsEmpty()) {
if (!nsJSUtils::GetCallingLocation(aCx, newInfo->mFilename, &newInfo->mLineNumber)) {
NS_WARNING("Failed to get calling location!");
}
}
nsAutoPtr<TimeoutInfo>* insertedInfo =
mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts));
LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n",
this, aTimeout, aIsInterval ? "yes" : "no"));
// If the timeout we just made is set to fire next then we need to update the
// timer, unless we're currently running timeouts.
if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) {
nsresult rv;
if (!mTimer) {
mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return 0;
}
mTimerRunnable = new TimerRunnable(this);
}
if (!mTimerRunning) {
if (!ModifyBusyCountFromWorker(true)) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
mTimerRunning = true;
}
if (!RescheduleTimeoutTimer(aCx)) {
aRv.Throw(NS_ERROR_FAILURE);
return 0;
}
}
return timerId;
}
IMPORTANT NOTE: The JavaScript instruction yield, has nothing to do with what I am talking about. I am talking about the sched_yield() functionality which happens when a binary process calls certain functions, such as sched_yield() itself, poll(), select(), etc.
I faced this issue with Firefox while toggling CSS classes using jQuery to control a CSS transition.
Increasing the duration of setTimeout to 50 from 0 helped, but as Alexis suggested this wasn’t 100% reliable.
The best (if longwinded) solution I found was to combine an interval timer with an IF statement to actually check whether the necessary styles had been applied before triggering the transition, rather using setTimeout and assuming execution had taken place in the intended order, e.g.
var firefox_pause = setInterval(function() {
//Test whether page is ready for next step - in this case the div must have a max height applied
if ($('div').css('max-height') != "none") {
clear_firefox_pause();
//Add next step in queue here
}
}, 10);
function clear_firefox_pause() {
clearInterval(firefox_pause);
}
In my case at least, this seems to work every time in Firefox.
In Firefox, the minimum value for setTimeout() calls is configurable and defaults to 4 in current versions:
dom.min_timeout_value The minimum length of time, in milliseconds,
that the window.setTimeout() function can set a timeout delay for.
This defaults to 4 ms (before 10 ms). Calls to setTimeout() with a
delay smaller than this will be clamped to this minimum value.
Values like 0 or 1 should behave like 4—no idea if that will cause delays in your code or just break it.
Context
I've got about 10 complex graphs which take 5sec each to refresh. If I do a loop on these 10 graphs, it takes about 50 seconds to refresh. During these 50 seconds, the user can move a scrollbar. If the scrollbar is moved, the refresh must stop and when the scrollbar stops to move, the refresh occurs again.
I'm using the setTimeout function inside the loop to let the interface refresh.
the algorithm is :
render the first graph
setTimeout(render the second graph, 200)
when the second graph is rendered, render the third one in 200ms, and so on
The setTimeout allows us to catch the scrollbar event and to clearTimeout the next refresh to avoid to wait 50sec before moving the scrollbar...
The problem is that it does not run anytime.
Take the simple following code (you can try it in this fiddle : http://jsfiddle.net/BwNca/5/) :
HTML :
<div id="test" style="width: 300px;height:300px; background-color: red;">
</div>
<input type="text" id="value" />
<input type="text" id="value2" />
Javascript :
var i = 0;
var j = 0;
var timeout;
var clicked = false;
// simulate the scrollbar update : each time mouse move is equivalent to a scrollbar move
document.getElementById("test").onmousemove = function() {
// ignore first move (because onclick send a mousemove event)
if (clicked) {
clicked = false;
return;
}
document.getElementById("value").value = i++;
clearTimeout(timeout);
}
// a click simulates the drawing of the graphs
document.getElementById("test").onclick = function() {
// ignore multiple click
if (clicked) return;
complexAlgorithm(1000);
clicked = true;
}
// simulate a complexe algorithm which takes some time to execute (the graph drawing)
function complexAlgorithm(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
document.getElementById("value2").value = j++;
// launch the next graph drawing
timeout = setTimeout(function() {complexAlgorithm(1000);}, 1);
}
The code does :
when you move your mouse into the red div, it updates a counter
when you click on the red div, it simulates a big processing of 1sec (so it freezes the interface due to javascript mono thread)
after the freezing, wait 1ms, and resimulate the processing and so on until the mouse move again
when the mouse move again, it breaks the timeout to avoid infinite loop.
The problem
When you click one time and move the mouse during the freeze, I was thinking that the next code that will be executed when a setTimeout will occurs is the code of the mousemove event (and so it will cancel the timeout and the freeze) BUT sometimes the counter of click gains 2 or more points instead of gaining only 1 point due to the mouvemove event...
Conclusion of this test : the setTimeout function does not always release resource to execute a code during a mousemove event but sometimes kept the thread and execute the code inside the settimeout callback before executing another code.
The impact of this is that in our real example, the user can wait 10 sec (2 graphs are rendered) instead of waiting 5 seconds before using the scrollbar. This is very annoying and we need to avoid this and to be sure that only one graph is rendered (and other canceled) when the scrollbar is moved during a render phase.
How to be sure to break the timeout when the mouse move ?
PS: in the simple example below, if you update the timeout with 200ms, all runs perfectly but it is not an acceptable solution (the real problem is more complex and the problem occurs with a 200ms timer and a complex interface). Please do not provide a solution as "optimize the render of the graphs", this is not the problem here.
EDIT : cernunnos has a better explanation of the problem :
Also, by "blocking" the process on your loop you are ensuring no event can be handled until that loop has finished, so any event will only be handled (and the timeout cleared) inbetween the execution of each loop (hence why you sometimes have to wait for 2 or more full executions before interrupting).
The problem is exactly contains in bold words : I want to be sure to interrupt the execution when I want and not to wait 2 or more full executions before interrupting
Second EDIT :
In summary : takes this jsfiddle : http://jsfiddle.net/BwNca/5/ (the code above).
Update this jsfiddle and provide a solution to :
Mouse move on the red div. Then click and continue moving : the right counter must raise only once. But sometimes it raises 2 or 3 times before the first counter can run again... this is the problem, it must raise only once !
The BIG problem here is setTimeout is unpredictable once it started, and especially when it is doing some heavy lifiting.
You can see the demo here:
http://jsfiddle.net/wao20/C9WBg/
var secTmr = setTimeout(function(){
$('#display').append('Timeout Cleared > ');
clearTimeout(secTmr);
// this will always shown
$('#display').append('I\'m still here! ');
}, 100);
There are two things you can do to minimize the impact on the browser performance.
Store all the intances of the setTimeoutID, and loop through it when you want to stop
var timers = []
// When start the worker thread
timers.push( setTimeout(function () { sleep(1000);}, 1) );
// When you try to clear
while (timers.length > 0) {
clearTimeout(timers.pop());
}
Set a flag when you try to stop process and check that flag inside your worker thread just in case clearTimeout failed to stop the timer
// Your flag
var STOPForTheLoveOfGod = false;
// When you try to stop
STOPForTheLoveOfGod = true;
while (timers.length > 0) {
clearTimeout(timers.pop());
}
// Inside the for loop in the sleep function
function sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if (STOPForTheLoveOfGod) {
break;
}
// ...
}
}
You can try out this new script.
http://jsfiddle.net/wao20/7PPpS/4/
I may have understood the problem but assuming you are trying to block the interface after a click for a minimum of 1 second and unblocking it by moving the mouse (after that 1 second minimum):
This is not a good implementation of sleep, as you are keeping the process running the whole time (doing nothing != sleeping), this results in a waste of resources.
Why not create an overlay (a semi/fully transparent div), put it on top of the rest of the interface (position fixed, full width and full height) and use it to prevent any interaction with the underlying interface. Then destroy it when the conditions are right (a second has passed and the user moved the mouse).
This behaves more like a sleep (has some initial processing time but then releases the processor for a given amount of time) and should help you achieve the behavior you need (assuming i understood it right).
It has the added bonus of allowing you to give the user some visual cue that some processing is being done.
Edit:
Also, by "blocking" the process on your loop you are ensuring no event can be handled until that loop has finished, so any event will only be handled (and the timeout cleared) inbetween the execution of each loop (hence why you sometimes have to wait for 2 or more full executions before interrupting).
Surprising enough you have not figured out that, when you setTimeout(); you can input a check after that. A variable is true then trash the wait, or trash it. Now there is a method that you can check to scroll with a scroll bar. After you have checked it true inside a variabled using the means, then you will find this will repeat inifite times as they scroll the bar, making many executing times of 5 seconds. To fix this add a 1 second wait to make sure it doesn't over repeat. Your welcome :)
Any long-running function is going to tie up your browser window. Consider moving your complexAlgorithm() outside of your main javascript code using WebWorkers.
The answer is in your question
...the refresh must stop and when the scrollbar stops to move, the
refresh occurs again.
You should write complexAlgorithm in such way that you can almost instantly brake it in a middle (just when you know you will have to re run)
so main code should look something like
stopAllRefresh; //should instantly(or after completing small chunk) stop refresh
setTimeout(startRefresh, 100);
and render graph in small chunks (each runs < 1sec) in setTimeout
like
var curentGraph = 0;
var curentChunk = 0;
function renderGraphChunk(){
if (needToBreak) //check if break rendering
{exit};
// Render chunk here
render(curentGraph, curentChunk);
curentChunk +=1;
setTimeout(renderGraphChunk, 1);
}
this is just a idea sketch, real implementation can be completely different
What you want to do can not be done without web worker, that is only implemented in some latest browser specially Chrome.
Otherwise, you have to break your algorithm in queue. Just like jQuery UI puts every next animation calculation in queue. http://api.jquery.com/jQuery.queue/
It is a simple queue and next instruction set is queued with help of setTimeout.
for (i=0; i <1000; i++)
{
process (i) ;
}
Can be translated to
function queue(s,n, f)
{
this.i=s;
this.n=n;
this.f=f;
this.step = function(){
if ( this.i <this.n)
{
this.f(this.i);
this.i = this.i +1;
var t = this;
setTimeout( function ( ) { t.step(); } , 5);
}
}
this.step();
}
queue ( O, 1000, function(i){
process(i);
}) ;
This is just an example of how Synchronous for loop can be written to execute same logic asynchronously using smaller independent iteration.
Try and check out web workers. I think it will be useful in this situation.
http://en.wikipedia.org/wiki/Web_worker
http://www.html5rocks.com/en/tutorials/workers/basics/
When looking to improve a page's performance, one technique I haven't heard mentioned before is using setTimeout to prevent javascript from holding up the rendering of a page.
For example, imagine we have a particularly time-consuming piece of jQuery inline with the html:
$('input').click(function () {
// Do stuff
});
If this code is inline, we are holding up the perceived completion of the page while the piece of jquery is busy attaching a click handler to every input on the page.
Would it be wise to spawn a new thread instead:
setTimeout(function() {
$('input').click(function () {
// Do stuff
})
}, 100);
The only downside I can see is that there is now a greater chance the user clicks on an element before the click handler is attached. However, this risk may be acceptable and we have a degree of this risk anyway, even without setTimeout.
Am I right, or am I wrong?
The actual technique is to use setTimeout with a time of 0.
This works because JavaScript is single-threaded. A timeout doesn't cause the browser to spawn another thread, nor does it guarantee that the code will execute in the specified time. However, the code will be executed when both:
The specified time has elapsed.
Execution control is handed back to the browser.
Therefore calling setTimeout with a time of 0 can be considered as temporarily yielding to the browser.
This means if you have long running code, you can simulate multi-threading by regularly yielding with a setTimeout. Your code may look something like this:
var batches = [...]; // Some array
var currentBatch = 0;
// Start long-running code, whenever browser is ready
setTimeout(doBatch, 0);
function doBatch() {
if (currentBatch < batches.length) {
// Do stuff with batches[currentBatch]
currentBatch++;
setTimeout(doBatch, 0);
}
}
Note: While it's useful to know this technique in some scenarios, I highly doubt you will need it in the situation you describe (assigning event handlers on DOM ready). If performance is indeed an issue, I would suggest looking into ways of improving the real performance by tweaking the selector.
For example if you only have one form on the page which contains <input>s, then give the <form> an ID, and use $('#someId input').
setTimeout() can be used to improve the "perceived" load time -- but not the way you've shown it. Using setTimeout() does not cause your code to run in a separate thread. Instead setTimeout() simply yields the thread back to the browser for (approximately) the specified amount of time. When it's time for your function to run, the browser will yield the thread back to the javascript engine. In javascript there is never more than one thread (unless you're using something like "Web Workers").
So, if you want to use setTimeout() to improve performance during a computation-intensive task, you must break that task into smaller chunks, and execute them in-order, chaining them together using setTimeout(). Something like this works well:
function runTasks( tasks, idx ) {
idx = idx || 0;
tasks[idx++]();
if( idx < tasks.length ) {
setTimeout( function(){ runTasks(tasks, idx); },1);
}
}
runTasks([
function() {
/* do first part */
},
function() {
/* do next part */
},
function() {
/* do final part */
}
]);
Note:
The functions are executed in order. There can be as many as you need.
When the first function returns, the next one is called via setTimeout().
The timeout value I've used is 1. This is sufficient to cause a yield, and the browser will take the thread if it needs it, or allow the next task to proceed if there's time. You can experiment with other values if you feel the need, but usually 1 is what you want for these purposes.
You are correct, there is a greater chance of a "missed" click, but with a low timeout value, its pretty unlikely.
When this function is called, the style change on the "gif" element does not show up until "lotsOfProcessing()" finishes. However, when I uncomment the alert("test"), the style change is shown before the alert pops up.
What I am trying to do is have an animated gif displayed while lotsOfProcessing is running. This seemed pretty straight forward solution but it is clearly not working. Any suggestions / solutions?
function nameOfFuntion()
{
document.getElementById("gif").style.display = "inline";
//alert("test");
lotsOfProcessing();
}
JavaScript code executes on the same thread as the browser's rendering. Everything that needs to be drawn waits for JavaScript execution to complete - including the next frame of any GIF animation.
The only solution is to break your long processing code down into smaller parts and delay each part using timers.
For example:
function nameOfFuntion() {
document.getElementById("gif").style.display = "inline";
//alert("test");
lotsOfProcessing();
}
function lotsOfProcessing() {
var i = 0;
window.setTimeout(function () {
partOfIntenseProcessing();
if (i < 1000000)
i++, window.setTimeout(arguments.callee, 10);
}, 10);
}
This will delay how long it will take for your processing to complete, but between timer execution the GIF can continue to animate.
You can also take a look at Web Workers, which allow you to run JavaScript operations in a background thread. However, they are not widely implemented yet (read: not available in Internet Explorer).
Perform your heavy processing in a delayed function with window.setTimeout():
function nameOfFunction()
{
document.getElementById("gif").style.display = "inline";
window.setTimeout(lotsOfProcessing, 10);
}
That's strange indeed. Seems like lotsOfProcessing gets javascript's single thread before the dom has time to refresh, but it's the first time I hear of something like that.
You might try this (not that is not an ideal solution):
function nameOfFuntion()
{
document.getElementById("gif").style.display = "inline";
setTimeout(lotsOfProcessing, 100);
}
This is a vaguely educated guess but it may be worth trying to put document.getElementById("gif").style.display = "inline"; into a function eg.
function nameOfFuntion()
{
showGif();
//alert("test");
lotsOfProcessing();
}
function showGif() {
document.getElementById("gif").style.display = "inline";
}
My thinking is that perhaps the lotsOfProcessing() is getting hoisted to the top of nameOfFunction() because it's a function and therefore getting processed first.