-1

I have searched high and low for a solution to this (that I can understand) and have yet to find one.

Fiddle here https://jsfiddle.net/the_o/a7dwp41s/

The Goal: To have the text change after a set period of time (for example 1 second).

The Problem: The timing is not accurate at all. It's not apparent in the fiddle but on my page the text sometimes changes waaaay too fast. Also sometimes the loop will just stop. I know that setTimeout is not accurate from reading other Stack Overflow answers, but have not come across a good solution for running a function after a set period accurately. I'd appreciate some help.

The HTML:

 <span class="text-center"><span class="top-line">A heading here</span>
<br><span class="bottom-line">There once was a...<br>
 <span id="changeTextMobile"></span></span>
</span>

The Javascript:

var text = ["carrot", "potato", "tomato", "lettuce", "radish", "cabbage", "melon", "cucumber"];
var elem = document.getElementById("changeTextMobile");

var counter = 0;

function rotate_text() {
    elem.innerHTML = text[counter];
    if (counter < 8) {
        counter++window.setTimeout(rotate_text, 1200);
    }
    if (counter == 8) {
        counter = 0;
    }
}

rotate_text();

Here's the fiddle again: https://jsfiddle.net/the_o/a7dwp41s/

T Ainsley
  • 13
  • 3
  • Javascript in browsers doesn't actually multithread. So the wait time for your "asynchronous" function in `setTimeout` can and will often be run at a different time than requested since the single JS thread has to pick when to actually break main execution and go back and run it. AFAIK there is no way to do a precise delay without blocking (and therefore locking up the entire browser). – CollinD Nov 01 '15 at 05:15
  • The classic approach here is to set a fine-grained timer (either with `setTimeout` or more likely `requestAnimationFrame`, then within that examine the current time and compute the difference with the previous time, and execute your action if the desired amount of time has passed. RAF will send your callback a high-resolution timer which can be used if you are interested in sub-millisecond precision. –  Nov 01 '15 at 05:34
  • *I have searched high and low for a solution to this * Funny, I searched for "javascript setimeout accuracy" and the following popped up immediately: http://stackoverflow.com/questions/21097421/what-is-the-reason-javascript-settimeout-is-so-inaccurate. Also see http://stackoverflow.com/questions/196027/is-there-a-more-accurate-way-to-create-a-javascript-timer-than-settimeout, which this question probably should be considered a duplicate of. –  Nov 01 '15 at 07:51
  • Hi torazaburo, thanks, I did see those threads and many others, but none of the code provided was close enough to what I wanted to do. I am very new to javascript, it was a bit beyond my abilities to rewrite the code in those answers for my own case. Thanks for your help. – T Ainsley Nov 02 '15 at 00:16

3 Answers3

1

The classic approach here is to set a fine-grained timer (either with setTimeout or more likely requestAnimationFrame, then within that examine the current time and compute the difference with the previous time, and execute your action if the desired amount of time has passed. RAF will send your callback a high-resolution timer which can be used if you are interested in sub-millisecond precision.

var text = ["carrot", "potato", ...];
var elem = document.getElementById("changeTextMobile");
var counter = 0;
var DELAY = 1000;
var old_timestamp = 0;

function rotate_text(timestamp) {
  if (timestamp > old_timestamp + DELAY) {  // HAVE WE WAITED LONG ENOUGH?
    update_vegetable();                     // CHANGE VEGETABLE NAME.
    old_timestamp = timestamp;              // REMEMBER WHEN WE DID THAT.
  }
  requestAnimationFrame(rotate_text);       // RINSE AND REPEAT
}

rotate_text(0);

where update_vegetable would look something like

function update_vegetable() {
  elem.textContent = text[counter++ % 8];
}

This should give you very accurate results. However, note that some browsers may slow down requestAnimationFrame when the tab is in the background. Also note that requestAnimationFrame may require vendor prefixing in certain browsers.

If you don't want to use requestAnimationFrame for some reason, you should be able to replace the call to it with setTimeout(tick, 16) with similar results.

Analysis of current code

In your current code, when counter reaches 8 and you reset it to 0, you are not calling setTimeout again to continue the sequence. That seems wrong.

if (counter == 8) {
    counter = 0;       //YOU ARE NOT RESETTING THE TIMEOUT.
}

In any case, you're better off using counter % 8 as another answer suggests and as shown above.

Also, the line below seems broken and is missing a semicolon. Is this your actual code? What it would do is add to counter the timer ID returned by setTimeout, which is completely meaningless.

counter++window.setTimeout(rotate_text, 1200);

should be

counter++;
window.setTimeout(rotate_text, 1200);
  • Thanks for pointing out for jQuery and for introducing RAF. Also just for my understanding, since it is high-resolution timer, it would be called several times per second. Now this will be very precise, but will it not affect working of other things? Like if I have a page with lots of element (2-3 carousels) and want to update them in similar fashion, what will be its impact? – Rajesh Nov 01 '15 at 07:57
  • 1
    @Rajesh The whole point of RAF is that it calls you back when there is the least chance of affecting the working of other things. Normal use cases will work fine. –  Nov 01 '15 at 08:01
0

Another way to accomplish your goal would be to use the setInterval() method instead of the setTimeout() method.

Documentation for the method is found on W3Schools.com

Unless I am misunderstanding your intention, you don't just want to change your text once, but repeatedly. In this case, setInterval() is the choice as setTimeout() is intended to only run a function once. This could be the source of your consistency issues.

If and when you wish to stop the timer's repeat functionality, use clearInterval() in your trigger.

MikeWu
  • 3,042
  • 2
  • 19
  • 27
  • Unfortunately `setInterval` will not work any more precisely than `setTimeout`. –  Nov 01 '15 at 07:29
0

You should use setInterval for such cases. I have updated JSFiddle.

setTimeout

setTimeout is used for cases when you have to add a delay before a function is executed.

setInterval

setInterval is used for case where you have to run certain function after certain time delay.

Also, just a note, setTimeout(function(){},0) does not mean it will be executed immediately. When you use these function, an event is registered to be executed at certain tick, so setTimeout(function(){},0) will be triggered on next tick and not immediately.

var text = ["carrot", "potato", "tomato", "lettuce", "radish", "cabbage", "melon", "cucumber"];
var counter = 0;

function initInterval() {
  interval = setInterval(function() {
    counter++;
    $("#changeTextMobile").text(text[counter % 8]);
  }, 1200);
}

$(document).ready(function() {
  initInterval();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<span class="text-center"><span class="top-line">A heading here</span>
<br><span class="bottom-line">There once was a...<br>
 <span id="changeTextMobile"></span></span>
</span>

Hope it helps.

Rajesh
  • 24,354
  • 5
  • 48
  • 79
  • 1
    OP stated *The Problem: The timing is not accurate at all.* In this respect, using `setInterval` is no better than using `setTimeout`. Also, please do not introduce jQuery when it's not necessary, the OP did not use it in his sample code, and the question is not tagged jQuery. –  Nov 01 '15 at 07:31