JavaScript setTimeout 0 blocking page rendering? - javascript

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.

Related

Why chrome's rendering result is different when using breakpoints?

At first, i was trying to figuring how to implement a "line-by-line shows" effect, as far as i know, chrome will reflow the page when there is a dom insert action, so i just think the following code could work:
Click the "Click Me" span tag, there will be some lines of "-.-" span occur one-by-one.
<!DOCTYPE html>
<html lang="en">
<body>
<div>
<span id="clickme">Click Me</span>
</div>
<div id="container"></div>
<script>
const container = document.getElementById("container");
const clickme = document.getElementById("clickme");
clickme.addEventListener("click", () => {
for (let i = 0; i < 10; ++i) {
const child = document.createElement("div");
for (let j = 0; j < 20; ++j) {
const span = document.createElement("span");
span.textContent = "-.- ";
child.appendChild(span);
}
container.appendChild(child);
}
});
</script>
</body>
</html>
Unfortunately, chrome seems to apply all the dom change in one batch, so i used performance recording to check if it really does, and here the result just verified the guess:
I was really confused by this result, so i tried again to add a breakpoint at the for-loop entry to see what happened in this process, and the result is completely different from the normal process:
Here i uploaded two screenshots because the rest ones is just the same render result (line-by-line).
So my question is why breakpoints change the behavior of chrome's rendering process, and is there any way to implement the "line-by-line shows" effect only with JavaScript?
(My original purpose is a little more complicated than "line-by-line", and cannot be solved by CSS transition)
When Chrome hits a breakpoint it will enter the special "spin the event loop" algorithm (or a variation of it at least).
This algorithm allows to pause the currently active task and let the event loop process other duties, such as updating the rendering. Note though that no other scripts will run while in this state. So you will have CSS animations update, new DOM changes painted to screen, but you won't e.g have scroll events, or requestAnimationFrame callbacks fire.
Also, note that the browser does not trigger a reflow at each DOM insertion. It will try to trigger a reflow only at the last moment when required (i.e right before painting in most cases). Though some Web-APIs can force such a reflow (e.g all the position getters like HTMLElement#offsetTop.
You can use setInterval as shown in your amended code below. This repeatedly interrupts the processing thread, those allowing the browser to refresh the display. When the number of desired lines is reached, you should invoke clearInterval.
<!DOCTYPE html>
<html lang="en">
<body>
<div>
<span id="clickme">Click Me</span>
</div>
<div id="container"></div>
<script>
const container = document.getElementById("container");
const clickme = document.getElementById("clickme");
clickme.addEventListener("click", () => {
let lines = 0;
const intervalID = setInterval(() => {
const child = document.createElement("div");
for (let j = 0; j < 20; ++j) {
const span = document.createElement("span");
span.textContent = "-.- ";
child.appendChild(span);
}
container.appendChild(child);
if (++lines >= 10) {
clearInterval(intervalID);
}
}, 500);
});
</script>
</body>
</html>

JavaScript page update while a script is running

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.

jQuery change html text by iterating over array

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.)

Why won't my HTML Images fade

I'm trying to create a simple slideshow effect. I have 10 images, and I've created a basic HTML page with 2 buttons to go to the right or left image. On clicking the button, the images change.
Now, I'm trying to add a basic fade functionality to the changing image. But the fade effect isn't getting displayed. When I put alerts, I notice that the fade is taking place, but without the alerts it is too fast to be visible. Also, it is happening on the previous image, instead of the next one.
<html>
<head>
<style>
.main {
text-align: center;
}
.centered {
display: inline-block;
}
#image {
border: solid 2px;
width: 500px;
height: 500px;
}
#number {
font-size: 30px;
}
</style>
<script>
function goLeft() {
var image = document.getElementById("image");
var pos = document.getElementById("number");
if(Number(pos.innerHTML)==1) {
image.src = "Images\\10.jpg"
pos.innerHTML = 10;
} else {
image.src = "Images\\" + (Number(pos.innerHTML)-1).toString() + ".jpg"
pos.innerHTML = (Number(pos.innerHTML)-1).toString();
}
for (var i=0; i<25; i++) {
setTimeout(changeOpacity(image, i), 1000);
}
}
function changeOpacity(image, i) {
alert(parseFloat(i*4/100).toString());
image.style.opacity = (parseFloat(i*4/100).toString()).toString();
}
function goRight() {
var image = document.getElementById("image");
var pos = document.getElementById("number");
if(Number(pos.innerHTML)==10) {
image.src = "Images\\1.jpg"
pos.innerHTML = 1;
} else {
image.src = "Images\\" + (Number(pos.innerHTML)+1).toString() + ".jpg"
pos.innerHTML = (Number(pos.innerHTML)+1).toString();
}
for (var i=0; i<25; i++) {
setTimeout(changeOpacity(image, i), 1000);
}
}
</script>
</head>
<body>
<div class="main">
<div class="centered">
<img id="image" src="Images\1.jpg">
</div>
</div>
<div class="main">
<div class="centered">
<span id="number">1</span>
</div>
</div>
<div class="main">
<div class="centered">
<button onclick="goLeft()" style="margin-right:50px;">Go Left</button>
<button onclick="goRight()" style="margin-left:50px;">Go Right</button>
</div>
</div>
</body>
The problem is this block of code that is in your goLeft method, and goRight method:
for (var i=0; i<25; i++) {
setTimeout(changeOpacity(image, i), 1000);
}
You are creating 25 timers that, and each timer will execute approximately 1 second later.
Creating animations is best left to the CSS.
In your CSS add:
#image {
transition: opacity 0.5s ease;
}
And then in your JavaScript, simply: image.style.opacity = 1.0;
When the opacity changes, CSS will automatically transition the opacity length at the speed defined in the css, e.g 0.5s. Feel free to experiment.
I also added a jsfiddle here: http://jsfiddle.net/dya7L8wq/
You misunderstood setTimeout and the for loop.
Norman's answer provides a good solution with CSS, but he doesn't talk too much about why your code is not working. So I'd like to explain.
for (var i=0; i<25; i++) {
setTimeout(changeOpacity(image, i), 1000);
}
You assumption is:
invoke changeOpacity(image, 0) after 1 second
invoke changeOpacity(image, 1) 1 second after step 1
invoke changeOpacity(image, 2) 1 second after step 2
invoke changeOpacity(image, 3) 1 second after step 3
....
And the last step is invoking changeOpacity(image, 24) 1 second after previous step.
What actually happens is:
The loop is finished almost immediately!
In each iteration, setTimeout queues an asynchronous function invocation, and it's done! That says, it will return right away, rather than wait until changeOpacity returns.
And then, after about 1 second, changeOpacity fires 25 times almost at the same time, because you queued it 25 times in the for loop.
Another problem here is: in changeOpacity invocations, passed-in parameter i are not 1, 2, 3...., they all have the same value that causes for loop to exit (1 second ago) - 25, because JS doesn't have a block scope prior to ES6 (in ES6 we have keyword let for it).
In a pure JS solution, to ensure the time sequence we'd usually queue next invocation at the end of every step:
function changeOpacity() {
// do something here
// before the function returns, set up a future invocation
setTimeout(changeOpacity, 1000)
}
Here's an example to print a list of numbers from 1 to 5:
var go = document.getElementById('go')
var op = document.getElementById('output')
var i = 0
function printNum() {
var p = document.createElement('p')
p.innerHTML = ++i
op.appendChild(p)
// next step
if(i < 5) {
setTimeout(printNum, 500)
}
}
go.onclick = printNum
<button id="go">GO</button>
<div id="output"></div>
Why use pure JavaScript?
Use jQuery.
It has a pretty neat fadeTo() function and a useful fadeIn() function.
Might wanna use that ;)

JavaScript: simple counter with pause between the values doesn't work

I'm starter in JavaScript and I try to make simple code to print numbers (0-100) , but with pause in printing, for every next number(for exp. 3 seconds pause).
Code doesn't work properly... It waits 3 seconds and print the last number (100 in my case). Can you help me, where is my mistake?
This is the code:
<html>
<head>
<script type="text/javascript">
function funkcija_polnac()
{
var i = 0;
while (i <= 100) {
setTimeout(function(){ document.write(i + '%');}, 3000);
i++;
}
}</script>
</head>
<body>
<div style="margin: 0px auto;" onclick="funkcija_polnac()">Start</div>
</body>
</html>
What your code does is schedule 101 function callbacks, all of which will happen one right after another about three seconds after the code runs, and all of which will use the i variable, not its value as of when the function was created. So after three seconds you get 101 iterations of the value 101. This is because the functions you're creating are "closures over" the i variable (more accurately, the context in which the variable was created), and so they have an enduring reference to the variable and see its value as of when they use it, not as of when they were created. More about closures on my blog: Closures are not complicated
Or at least, that's what you'd see if it weren't that document.write, when used after initial parsing, blows away the page entirely. Basically: Don't use document.write. :-)
To fix it, you would schedule a single call to a function that, once it's run, schedules the next call. And you'd use the DOM or similar rather than document.write to see the output.
Example:
// NOTE: I used 30ms rather than 3000ms so it runs faster
var i = 0;
showOne();
function showOne() {
display(i);
++i;
if (i <= 100) {
setTimeout(showOne, 30); // You'd really want 3000
}
}
// Displays the given message by adding a paragraph element to the page
function display(msg) {
var p = document.createElement('p');
p.innerHTML = msg;
document.body.appendChild(p);
}
p {
padding: 0;
margin: 0;
font-family: sans-serif;
}
Well, you are running your while before the timeout happens. Try this
<html>
<head>
<script type="text/javascript">
function funkcija_polnac(i)
{
document.getElementById("output").innerHTML = i + "%";
if (--i > -1) {
setTimeout(function () { funkcija_polnac(i); }, 3000);
}
}
</script>
</head>
<body>
<div style="margin: 0px auto;" onclick="funkcija_polnac(5)">Start</div>
<div id="output"></div>
</body>
</html>

Categories