as implied in the title, I have a function that is going to be called multiple times quickly. However it must not execute less than 1 second after it has been called lastly.
I've made the following code but it seems messy to me and I feel like there must be something better...
let available = true
let requested = 0
function notTwiceASecond() {
if (available) {
console.log('Executed')
// ...
available = false
setTimeout(() => {
available = true
if (requested) {
notTwiceASecond()
requested--
}
}, 1000)
} else {
console.log('Still on cooldown')
requested
}
}
for (let i = 0; i < 5; i ) {
notTwiceASecond()
}
Although it works just fine, after doing some research I think the way to go is using async/await but I'm fairly inexperienced with JS and don't know how to implement it properly.
CodePudding user response:
The question
Like a debouncer, you don't want to immediately execute on each call. But unlike a debouncer, you want to have each call queued for future execution.
Like a scheduler, you want to specify when to call the queued calls. More specifically, you want to execute calls with a fixed delay inbetween each call.
This means we can take inspiration from different ideas:
- From debouncers: How to not execute on every call.
- From queues: How to organize calls (first-in-first-out (FIFO) is what we want).
- From schedulers: How to manage calls.
The answer!
Simple debouncing can be done with a delayed action. In JS we can use setTimeout()
for the delayed action. Do note that a regular debounce will act as if only the last call has been made, like this:
let timeoutId = undefined;
function debounce(callback) {
// Clear previous delayed action, if existent
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
// Start new delayed action for latest call
timeoutId = setTimeout(() => {
callback();
timeoutId = undefined; // Clear timeout
}, 1000);
}
const callback = () => console.log("debounced!");
for (let i = 0; i < 10; i) {
debounce(callback);
}
But we don't want to dismiss past calls. Actually, we want to act on the first call, and enqueue future calls!
const queue = [];
let timeoutId = undefined;
function weirdDebounce(callback) {
if (timeoutId === undefined) {
timeoutId = setTimeout(() => {
callback();
timeoutId = undefined;
console.log("[DEBUG] Queue-length:", queue.length);
}, 1000);
} else {
queue.push(callback);
}
}
const callback = () => console.log('"debounced"!');
for (let i = 0; i < 10; i) {
weirdDebounce(callback);
}
Now that "act on first call" and "enqueue further calls" is in place, we only need to actually execute the enqueued calls after some delay.
const queue = [];
let timeoutId = undefined;
function weirdDebounceAndSchedule(callback) {
function pauseThenContinue() {
timeoutId = setTimeout(function() {
timeoutId = undefined;
// Hope that your browser can tail-optimize doNext()
// to not run into potential stack-overflows!
doNext();
}, 1000);
}
function doNext() {
// Only do next if available and no delayed action is in progress
if (queue.length && timeoutId === undefined) {
const nextCallback = queue.shift();
nextCallback();
pauseThenContinue();
}
}
queue.push(callback);
doNext();
}
const callback = () => console.log("now scheduled!");
for (let i = 0; i < 10; i) {
weirdDebounceAndSchedule(callback);
}
The final answer?
Maybe you want to be able to have different such "debounced" functions, and maybe you want to have them run independently of each other. If so, you might want to use the following implementation.
From the previous explanations, I hope it is understandable how this one work, though the previous explanations are actually derived from this code:
/**
* Create a scheduler for calling callbacks with a fixed delay inbetween each call.
*
* @param milli the inbetween-delay in milliseconds
* @returns a function to add callbacks to the queue
*/
function withPause(milli) {
const queue = [];
let timeoutId = undefined;
function pauseThenContinue() {
timeoutId = setTimeout(
function() {
timeoutId = undefined;
doNext();
},
milli
);
}
function doNext() {
if (queue.length && timeoutId === undefined) {
queue.shift()();
pauseThenContinue();
}
}
return function(callback) {
queue.push(callback);
doNext();
};
}
// Can be used like this:
const doTask = withPause(1000);
for (let i = 0; i < 4; i) {
doTask(() => console.log("example"));
}
// Or like this, with Function.prototype.bind():
const notTwiceASecond = doTask.bind(null, () => console.log("not twice"));
for (let i = 0; i < 4; i) {
notTwiceASecond();
}
To add its independence of other such schedulers, I make use of closures; specifically, I use a closure for each scheduler to have its own for queue
, timeoutId
and milli
.
Also, to have it alias a function with the same name as your notTwiceASecond()
function, I make use of Function.prototype.bind()
and am assigning it to a similarly named variable.
Improve it!
Obviously my code is not final. Further, it isn't even the only way to do it, and most likely not the best!
For example, the delay is fixed upon "scheduler creation"! You can't first wait for 1s, then for 3s, then for 2s, ...
Maybe this is something you want, or just an exercise idea for you!
CodePudding user response:
If you're using a button or HTML element to trigger your function, you can simply do this:
const anotherFunction = () => {
console.log('hi');
}
const attemptFunction = () => {
anotherFunction()
document.getElementById("button").disabled = true;
setTimeout(() => {
document.getElementById("button").disabled = false;
}, 1e3)
}
<button id="button" onclick="attemptFunction()">text</button>
If you're calling this function using JavaScript or something else (the button is demonstrative):
var available = true;
const anotherFunction = () => {
console.log('hi');
}
const attemptFunction = () => {
if(available == true) {
anotherFunction();
available = false;
setTimeout(() => {
available = true;
}, 1e3)
}
return false;
}
<button id="button" onclick="attemptFunction()">text</button>
CodePudding user response:
You can try something like this:
const delay = 1000;
let executedAt = 0;
function fancyFunction(){
if ((executedAt delay) < Date.now()){
// this is where you can execute your code
console.log('Executing...');
executedAt = Date.now();
}
}
// just a test where you will notice the function above
// only executes once a second.
setInterval(function(){
fancyFunction();
},200);
CodePudding user response:
I think this is exactly what you asked for, but a lot simpler. Recall the function will cancel it previous call and put a new one in line:
let timeOutId;
function notTwiceASecond() {
clearTimeout(timeOutId);
timeOutId = setTimeout(() => {
console.log('test');
},1000);
}
You can test on jsbin.com if you want:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button onclick='notTwiceASecond()'> Test </button>
</body>
<script>
let timeOutId;
function notTwiceASecond() {
clearTimeout(timeOutId);
timeOutId = setTimeout(() => {
console.log('teste');
},1000);
}
</script>
</html>