How to Make 'Setinterval' Behave More in Sync, or How to Use 'Settimeout' Instead

setInterval vs setTimeout which is better to run a loop at a specified speed

You can use async/await to emulate typing and create delays:

//
// Async timeout function
const timeout = ms => new Promise(resolve => setTimeout(resolve, ms));

//
// Bot typing function
const runBot = text => {
// Use promises
return new Promise(async (resolve, reject) => {
// DOM
const div = document.getElementById('text');

// Create chars array
const chars = text.split('');

// Wait 1 sec before typing
await timeout(1000);

// Type text
while(chars.length) {
// Put one char at iteration
div.innerHTML += chars.shift();

// Wait some amount
await timeout(100);
}

// Add new line to the text box
div.innerHTML += '<br>'

// Finish function
return resolve();
});
}

//
// Text typing runner and button controller
const typeText = async text => {
// DOM
const button = document.querySelector('button');

// Disable button
button.disabled = true;

// Run bot and wait till it finishes
await runBot(text);

// Enable button
button.disabled = false;
}
<div id="text" style="padding:10px;border:1px solid #000"></div>
<button onClick="typeText('This is demo text!')">Start bot</button>

setTimeout or setInterval?

They essentially try to do the same thing, but the setInterval approach will be more accurate than the setTimeout approach, since setTimeout waits 1000ms, runs the function and then sets another timeout. So the wait period is actually a bit more than 1000ms (or a lot more if your function takes a long time to execute).

Although one might think that setInterval will execute exactly every 1000ms, it is important to note that setInterval will also delay, since JavaScript isn't a multi-threaded language, which means that - if there are other parts of the script running - the interval will have to wait for that to finish.

In this Fiddle, you can clearly see that the timeout will fall behind, while the interval is almost all the time at almost 1 call/second (which the script is trying to do). If you change the speed variable at the top to something small like 20 (meaning it will try to run 50 times per second), the interval will never quite reach an average of 50 iterations per second.

The delay is almost always negligible, but if you're programming something really precise, you should go for a self-adjusting timer (which essentially is a timeout-based timer that constantly adjusts itself for the delay it's created)

How to use setInterval function within for loop

So, a few things:

  1. Most importantly, the callback function you've passed to setInterval() maintains a reference to x rather than the snapshot value of x as it existed during each particular iteration. So, as x is changed in the loop, it's updated within each of the callback functions as well.
  2. Additionally, for...in is used to enumerate object properties and can behave unexpectedly when used on arrays.
  3. What's more, I suspect you really want setTimeout() rather than setInterval().

You can pass arguments to your callback function by supplying additional arguments to setTimout():

var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]);

Numbers will be passed by value rather than reference. Here's an example:

var list = [1,2,3,4];
for (var x = 0, ln = list.length; x < ln; x++) { setTimeout(function(y) { console.log("%d => %d", y, list[y] += 10); }, x * 500, x); // we're passing x}

nested settimeout with irregular intervals (more sophisticated way)

Because your animate calls are always to the same element, and the set backgroundColor is predictable (transparent to black to transparent, and so on), all of that functionality can be abstracted into a function. To chain it easily, you could have that function return a Promise that resolves after the desired amount of time, allowing you to use .thens or await without callback nesting. In addition, because you often do many animations (after delays) in a row, you could pass an array of the number of ms to wait between each animation, and use a loop to animate and then await a Promise that resolves after that many ms.

Something else that can reduce indentation hell is to return early when you have an alternation between a long code block and a short code block. That is, as with your if (myRandomNumberBetween(1, 100) > 10) { tests, instead of

if (test1) {
// many lines 1
if (test2) {
// many lines 2

// many more lines
} else {
// do something else 1
} else {
// do something else 2
}

It's pretty unreadable - which test is each do something else connected to? It's not immediately obvious, which is a problem. Rather, you can do something like:

if (!test) {
// do something else 1
return;
}
// many lines 1
if (!test2) {
// do something else 2
return;
}
// many lines 2

Translating all of this to a fix of your original code:

// Animation part:
let isBlack = true;
function animate() {
$(".playerInfoCoatTwo").animate({
backgroundColor: isBlack ? "transparent" : 'black'
}, 20);
isBlack = !isBlack;
}

// Delay function,
// allows for much flatter code when you can `await` a `Promise` compared to `setTimeout`:
const delay = ms => new Promise(res => setTimeout(res, ms));
async function animateAndWait(msArr) {
for (let i = 0; i < msArr.length; i++) {
animate();
await delay(msArr[i]);
}
}

async function betterBlitzColor() {
await animateAndWait([0, 300, 50, 150, 50, 50]);
if (myRandomNumberBetween(1, 100) <= 10) {
return delay(myRandomNumberBetween(9000, 18000))
.then(betterBlitzColor);
}
await animateAndWait([650, 300]);
if (myRandomNumberBetween(1, 100) <= 10) {
return delay(myRandomNumberBetween(9000, 18000))
.then(betterBlitzColor);
}
await animateAndWait([400, 100, 100, 150]);
delay(myRandomNumberBetween(9000, 18000))
.then(betterBlitzColor);
}

A more performance-friendly way to use setInterval() in componentDidMount when fetching data

On the frontend side, my advice would be to not use setInterval, but use setTimeout instead.

Using setInterval, your app might send another request even if the response for previous request hasn't come back yet (e. g.: it took more than 1 second). Preferably, you should only send another request 1 second after the previous response is received.

componentDidMount() {
getData();
}

getData = () => {
fetch().then(() => {
updateState();

// schedule next request
setTimeout(getData, 1000);
});
}

You should also try to reduce the amount of updates that need to be done on the frontend, for example by reducing the number of the data.

But nevertheless, I think the most important is to rethink the design of your application. Polling huge JSON that is going to only grow bigger is not a scalable design. It's bad for both the server and the client.

If what you are trying to do is to have the client be notified of any changes in the server side, you should look into WebSocket. A simple idea is that the browser should establish a WS connection to the server. Upon any updates to the server, instead of sending down the whole data, the server should only send down the updates to the client. The client would then update its own state.

For example, let's say 2 users are opening the same page, and one user make changes by adding a new Product. Server will receive this request and update the database accordingly. It will also broadcast a message to all open WebSocket connections (except for the one connection that added the Product), containing a simple object like this:

{
"action": "INSERT",
"payload": {
"product": {
"id": 123123,
... // other product data
}
}
}

The other user will use this data to update its own state so it matches the server.

setTimeout timing precision

Js has 3 microtask queues, these are(were) setTimeout/Interval/Immediate (some people call these macrotask, etc whatever), requestAnimationFrame (rAF) and the new child Promises. Promises resolve asap, setTimeouts have 4ms min difference between successive invocations if they are nested (& more than 5 layers deep), rAF will execute around 60 frames per second.

Amongst these rAF is aware of document.hidden state and roughly executes every ~17ms (16.67 theoretically). If your desired intervals are larger than this value, settle with rAF.

The problem with rAF is, since it executes every ~17ms, if I would want to execute something with 100 ms intervals,then after 5 ticks I would be at ~85ms, at the sixth tick I'd be at 102ms. I can execute at 102ms, but then I need to drop down 2ms from the next invocation time. This will prevent accidental 'phasing out' of the callback with respect to the frames you specify. You can roughly design a function that accepts an options object:

function wait(t,options){
if(!options){
options = t;
window.requestAnimationFrame(function(t){
wait(t,options);
});
return options;
}
if(!options._set) {
options.startTime = options.startTime || t;
options.relativeStartTime = options.startTime;
options.interval = options.interval || 50;
options.frame = options.frame || 0;
options.callback = options.callback || function(){};
options.surplus = options.surplus || 0;
options._set = true;
}
options.cancelFrame = window.requestAnimationFrame(function(t){
wait(t,options);
});
options.elapsed = t - options.relativeStartTime + options.surplus;
if (options.elapsed >= options.interval) {
options.surplus = options.elapsed % options.interval;
options.lastInvoked = t;
options.callback.call(options);
options.frame++;
options.relativeStartTime = t;
}
return options;
}

The object gets recycled and updated at every invocation. Copy paste to your console the above and try:

var x = wait({interval:190,callback:function(){console.log(this.lastInvoked - this.relativeStartTime)}})

The callback executes with this pointing to the options object. The returned x is the options object itself. To cancel it from running:

window.cancelAnimationFrame(x.cancelFrame);

This doesn't always have to act like interval, you can also use it like setTimeout. Let's say you have variable frames with multiples of 32 as you said,in that case extend the options object:

var x = wait({frameList:[32,32,64,128,256,512,1024,2048,32,128],interval:96,callback:function(){
console.log(this.lastInvoked - this.relativeStartTime);
window.cancelAnimationFrame(this.cancelFrame);
this.interval = this.frameList[this.frame];
if(this.interval){
wait(this);
}
}})

I added a frameList key to the options object. Here are some time values that we want to execute the callback. We start with 96, then go inside the frameList array, 32,32, 64 etc. If you run the above you'll get:

99.9660000000149
33.32199999992736
33.32199999992736
66.64400000008754
133.28799999994226
249.91499999980442
517.7960000000894
1016.5649999999441
2049.7950000001583
33.330000000074506
133.31999999983236

So these are my thoughts about what I'd do in your situation.

it runs as close as possible to the specified interval. If you put very close intervals such as 28,30,32 you will not be able to inspect the difference by eye. perhaps try console logging the 'surplus' values like this:

var x = wait({interval:28,callback:function(){
console.log(this.surplus);
}})

You will see slightly different numbers for different intervals, and these numbers will shift in time, because we are preventing 'phasing out'. The ultimate test would be to look at the average time takes for certain amount of 'frames':

var x = wait({interval:28,callback:function(){
if(this.frame === 99){
console.log(this.lastInvoked - this.startTime);
window.cancelAnimationFrame(this.cancelFrame);
}
}}) //logs something around 2800

if you change the interval to 32 for instance, it will log something around 3200ms etc. In conclusion, the function we design should not depend on what the real time is, it should get from the js engine which frame we are currently at, and should base its pace on that.



Related Topics



Leave a reply



Submit