I've been reading about the single-thread nature of javascript and how the UI will not update until the code is complete (eg. this question). The responses to that question mainly explained how the code executes, but didn't provide a real viable solution outside of some "hacky" timers.
I'm wondering if there is a good solution, that may include promises, or .then() functionality that will "paint" the UI prior to javascript execution.
The following is a specific example: http://jsfiddle.net/zbtsd12k/. In this example I would expect the "Change Color" to change immediately, prior to the calculation being run.
WARNING: My example includes a for-loop of one billion iterations (which took < 2 seconds on my machine) and may freeze your computer for a bit.
Any help would be appreciated.
A few changes
Added a setTimeout around the "logic" so that the DOM has a chance to redraw
Broke apart the then(makeCalculations()) line because #1 updateClass() does not return a promise and #2 then(methodCall()) would execute the methodCall immediately because you put () on the end of it.
function updateClass(element, newClass) {
element.addClass(newClass);
return true;
};
function makeCalculations() {
$("p").text("Running...");
setTimeout(function() {
var x = 0;
for (var i = 0; i < 1000000000; i++) {
x += i;
}
$("p").html("Done!" + "<br />" + "x = " + x);
}, 10);
}
$(function() {
$("button").click(function() {
updateClass($("span"), "purple");
makeCalculations();
});
});
.purple {
color: purple;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<span>Change Color</span>
<br>
<button>Click Me</button>
<p>Click the button</p>
Related
According to this StackOverflow question
Changing the DOM is synchronous. Rendering the DOM actually happens after the JavaScript stack has cleared.
and according to this google doc the screen fresh rate 60fps is equivalent to roughly refreshing every 16ms, I write this example:
<!DOCTYPE html>
<html>
<head>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelector('#do').onclick = function() {
document.querySelector('#status').innerHTML = 'calculating...';
// setTimeout(long, 0); // will block
setTimeout(long, 1); // will not block
};
function long(){
let result = 0
for (let i = 0; i < 1000; i++) {
for (let j = 0; j < 1000; j++) {
for (let k = 0; k < 1000; k++) {
result += i + j + k;
}
}
}
document.querySelector('#status').innerHTML = 'calculation done';
document.querySelector('#result').innerHTML = result.toString();
}
});
</script>
</head>
<body>
<button id='do'> Do long calc!</button>
<div id='status'></div>
<div id='result'></div>
</body>
</html>
with jsfiddle link
I played around with the code and found that blocking happens with time delay under 12ms, and happens more often with lesser delay.
I have two different ways of comprehension:
In this case only setTimeout with time delay over 16ms should not block, time delay of 0 and 1 are way less than 16ms so both of them should block;
Right after setTimeout call and long pushed to message queue (with optional delay), now the call stack is empty so in both cases setTimeout should not block and 'calculating...' always be rendered.
What's wrong with my understanding?
It's possibly to do with your browser throttling the delay.
It reads as though most browsers disregard a value of 0, might be worth looking at the DOM_MIN_TIMEOUT_VALUE for your browser.
I have a web page that does a lot of calculations as part of it's loading.
I want to display a spinner bar while the page is "thinking" so that the end user knows the delay is to be expected.
The way I was going to do this is to display a GIF file that would be a spinning wheel, and hide the table that would be my output.
The problem is, once the function starts, updates to the page seem to freeze until the function is done. Therefore, the end user never sees the "In Process" section.
The sample code I put together to demonstrate my problem is:
<!DOCTYPE html>
<html>
<body>
<script>
function show(id, value) {
document.getElementById(id).style.display = value ? 'block' : 'none';
}
function Processing(x)
{
if (x === true)
{
show('page', false);
show('loading', true);
}
else
{
show('page', true);
show('loading', false);
}
}
function MainProcess()
{
Processing(true) // Set to "Show processing..."
var start = new Date().getTime(); // Sleep a few seconds
for (var i = 0; i < 5; i++) {
if ((new Date().getTime() - start) < 3000) { i = 1 }
}
Processing(false) // Set to "Show Completed processing..."
}
window.onload = function() {
show('page', false);
show('loading', false);
};
</script>
<div id="loading">
<h1>Processing your request.</h1>
</div>
<div id="page">
<DIV class="body">
<h3>Done Processing your request.</h3>
</DIV>
</div>
<div id="buttons">
<button onclick="MainProcess();">Start Timer</button>
<button onclick="Processing(false);">Set to Completed</button>
<button onclick="Processing(true);">Set to Processing</button>
</body>
</html>
When I run this, it presents three buttons.
When you hit "Start Timer", it should show processing for a few seconds, then display done. Instead, the button changes colors and otherwise appears to do nothing till the timer is done, and it displays complete.
(I am new to JavaScript but a seasoned pro in several other languages. Is there a "DoEvents" type of call I can make like in Visual Basic or Powershell?)
The problem seems to be that the DOM update (i.e. changing the display properties on your DIVs) doesn't complete before your processing function starts. One option would be to use window.setTimeout to delay the start of your processing function to allow the DOM update to complete:
function MainProcess()
{
Processing(true); // Set to "Show processing..."
// Pause for 100 ms before starting time-consuming code to allow dom update to c
var domUpdateDelay = 100;
window.setTimeout(function() {
var start = new Date().getTime(); // Sleep a few seconds
for (var i = 0; i < 5; i++) {
if ((new Date().getTime() - start) < 3000) { i = 1 }
}
Processing(false) // Set to "Show Completed processing..."
}, 100);
}
window.setTimeout takes two arguments: the first argument is a function that runs after x milliseconds, where x is the second argument.
The problem with this approach is that an appropriate delay for setTimeout will vary across machines/users/browsers/runs. However, my guess is that this will probably not be an issue in 99% of cases.
I think you can take advantage of a function like setTimeout (https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout), say:
setTimeout(function() {
Processing(false);
}, 5000);
I think what's happening is you're nearly crashing the browser with that loop. I may have misunderstood what parts of your question were just there for demonstration purposes.
If I write the html:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<h1 id="message">
</h1>
and the JS:
messages = ["Here", "are", "some", "messages."]
$(function() {
for (var i = 0; i < messages.length; i++) {
$('#message').html(messages[i]).delay(1000);
}
});
and load the page, I expect to see each string in the array show up with a delay in between. However, all I see is "messages." appear. It seems that the for loop iterates immediately through each value in the array before performing any delay.
I have seen another method for getting the desired visual result (How can I change text after time using jQuery?), but I would like to know why the earlier method does not work. What is going on when this code is executed?
This is how i would delay my message changing.
function delayLoop(delay, messages) {
var time = 100;
$(messages).each(function(k, $this) {
setTimeout(function()
{
$("#message").html($this);
}, time)
time += delay;
});
}
delayLoop(1000, ["Here", "are", "some", "messages."]);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="message">
</div>
All I did was for each message delay by an additional delay time.
It works in async mode so its not ui blocking and the messages will display a second after one another.
EDIT:
Removed the .delay from the .html it is redundant.
Note that jQuery's delay is specifically for effects; methods like html do not use the effects queue and are therefore not affected by delay.
This is a problem better solved with JavaScript's native setTimeout function. There are many ways to do this; in fact, you don't even need jQuery!
let messages = ["Here", "are", "some", "messages."];
let delay = 1000;
let header = document.getElementById("message");
messages.forEach(function(message, i) {
setTimeout(function() {
header.innerText = message;
}, delay * i);
});
<h1 id="message" />
You would need something along the lines of
$(function() {
for (var i = 0; i < messages.length) {
var done=false;
$('#message').html(messages[i]).delay(1000).queue(function(){
done=true;
$(this).dequeue();
});
if(done==true){
i++;
}
}
});
Thank you for the answers and comments--very helpful.
I also found this post helpful: Node.js synchronous loop, and from it wrote this (which also works):
function changeText() {
var msg = messages.shift();
$('#message').html(msg).show(0).delay(1000).hide(0, function() {
if (messages.length > 0) {
changeText();
}
});
}
(I used .show and .hide because without them only one of the array values appeared. I'm not sure why that is, but that's a question for another time.)
I am getting an issue with a large amount of processing causing the non-responsive script error in IE8 (and no, I cannot make the users use a better browser).
I then read that it should be possible to split up the tasks and cede control back to the browser in between different parts of the validation. So I decided to make a simple example based on some code I found to figure out where the breaking points are. The real code is doing lots of jquery validationengine processing.
I tried to use jsFiddle but I can't get jsFiddle to run in IE8. Bummer. So, I'll have to share inline here.
When I first load it, it seems to work just fine. I push the button and both functions finish without a problem. However, subsequent pushes causes an unresponsive script error. I've played around with the number of loops in my simulated work function. Much more than 1.25 million loops and it dies with unresponsive script.
Shouldn't separate calls to the onClick start the non-responsive counter anew? What am I missing here?
<html>
<head>
<script>
var progress = null;
var goButton = null;
window.onload = function() {
progress = document.getElementById("progress");
goButton = document.getElementById("goButton");
}
function runLongScript(){
// clear status
progress.value = "";
goButton.disabled=true;
var tasks = [function1, function2];
multistep(tasks,null,function() {goButton.disabled=false;});
}
function function1() {
var result = 0;
var i = 1250000;
for (;i>0; i--) {
result = result + 1;
}
progress.value = progress.value + "f1 end ";
}
function function2() {
var result = 0;
var i = 1250000;
for (;i>0; i--) {
result = result + 1;
}
progress.value = progress.value + "f2 end";
}
function multistep(tasks, args, callback){
var tasksClone = tasks.slice(0); //clone the array
setTimeout(function(){
//execute the next task
var task = tasksClone.shift();
task.apply(null, args || []);
//determine if there's more
if (tasksClone.length > 0){
setTimeout(function () {
multistep(tasksClone, args, callback);
}, 100);
} else {
callback();
}
}, 100);
}
</script>
</head>
<body>
<p><input type="button" id="goButton" onClick="runLongScript();" value="Run Long Script" /></p>
<input type="text" id="progress" />
</body>
</html>
You're never calling clearTimeout() to remove the one currently running when the button has been pressed already. Add an if statement before you start another setTimeout and check to see if one is already running, clear it if it is, and then continue. Here's a link that should help you if you have any questions: https://developer.mozilla.org/en-US/docs/Web/API/window.clearTimeout
I tried doing a JS fade effect with the setTimeout() function and it appeared to work to some extent, but I can't seem to figure out what's wrong with the code below:
<html>
<head>
<title></title>
<script type="text/javascript">
function FadeEffect(n)
{
var i=1;
fade = document.getElementById("box");
if (n===1)
{
fade.style.opacity=i/10;
i++;
setTimeout("FadeEffect(1)",50);
if (fade.style.opacity=1)
{
var i=1;
}
}
else if (n===0)
{
fade.style.opacity=1-i/10;
i++;
setTimeout("FadeEffect(0)",50);
if (fade.style.opacity=0)
{
var i=1;
}
}
}
</script>
<style type="text/css">
#box{
width: 200px;
height: 50px;
border: 1px solid black;
background-color: #ccc;
opacity: 0;
}
</style>
</head>
<body>
<div onMouseOver="FadeEffect(1)" onMouseOut="FadeEffect(0)" id="box">Menu</div>
</body>
</html>
Edit: updated the code with setTimeout() functions.
There are two problems with the function that I can see.
First, your if statements both do an assignment rather than a comparison. You are saying if (n=0) (one =, assignment) when you should be saying if (n===0) (three ===, comparison, or you can use two == for a type-converting comparison).
Second, using a for loop to repeatedly change the style isn't going to fade because the browser doesn't update the display at the same time as your code is executing - essentially it uses the same thread for display and for JavaScript. So the page will be updated after the function exits. You need to give the browser a chance to update after each iteration by using setTimeout() - something like:
function fadeEffect(element,startValue,target,delay){
element.style.opacity = startValue;
if (startValue < target)
startValue = Math.min(startValue + 0.1, target);
else
startValue = Math.max(startValue - 0.1, target);
if (startValue != target)
setTimeout(function(){fadeEffect(element,startValue,target,delay);}, delay);
}
<div onMouseOver="fadeEffect(this, 1, 0, 100);"
onMouseOut="fadeEffect(this, 0, 1, 100);" id="box">Menu</div>
Demo: http://jsfiddle.net/hLQ6y/2/
EDIT: Note that this code doesn't cope all that brilliantly if you move the mouse in and out too quickly, i.e., if you trigger the fade in before the fade out has finished. (You can see what I mean in my jsfiddle.) You can solve this by saving the return from .setTimeout() and calling .clearTimeout() if required. Given that I've already covered the essence of the question I'll leave the fine-tuning as an exercise for the reader...
UPDATE: Your updated code has introduced new if statements with the same assignment-instead-of-comparison problem. Also it is calling setTimeout() forever - you should do that conditionally as in the answers Pointy and I gave. Also you seem to be relying on the variable i which, as a local variable, will not retain its value between calls - you could make it a global, but better to manage it as a parameter like I did or as a local variable in an outer function like Pointy did.
The problem is that all those changes to the style will happen before the browser bothers to update the display. What you need to do is space out the changes over a much, much longer period of time using "setTimeout()".
function fadeIn() {
function increment() {
box.style.opacity = Math.min(1.0, (opacity += 0.1));
if (opacity < 1.0) {
setTimeout(increment, 100);
}
}
var box = document.getElementById('box'), opacity = 0;
box.style.opacity = opacity;
setTimeout(increment, 100);
}
edit — Here is a jsfiddle to demonstrate.
If you want it to fade, you need to create a function that sets the opacity -0.1 or +0.1 and then calls itself with setTimeout in 100ms (or fewer). If you don't let the script wait, it will be too fast and immediately set the opacity to 1.0.
Anyways, you could do this a lot easier using jQuery.