admin 管理员组文章数量: 1086019
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.
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.
Share Improve this question asked Mar 25, 2022 at 22:32 ScaarScaar 555 bronze badges 5-
8
This is often referred to as
debouncing
. That might help your searching. – CollinD Commented Mar 25, 2022 at 22:35 - 1 Debounce is a term I have seen used to describe this functionality. Might be something there to help you. – iamsimonsmale Commented Mar 25, 2022 at 22:36
- You did well, async/await would not help your code a whole lot, it's absolutely fine! – Jakub Kotrs Commented Mar 25, 2022 at 22:36
- This is actually the opposite of how debouncing usually works. With debouncing, you wait until you stop calling the function for some time, then execute the last one. A good example is when you're doing auto-fill -- you wait until the user stops typing before filling in. – Barmar Commented Mar 25, 2022 at 22:50
-
Whether
async
/await
is the right way to go depends on what you're trying to achieve in the bigger picture. From the question itself I cannot say whether it would be a good idea to go that way or not. – Oskar Grosser Commented Mar 26, 2022 at 1:39
4 Answers
Reset to default 7The 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.
With async
/await
Using async
/await
is a great idea! Note that in JS there is no way to sleep()
natively because of its original synchronous design. We could either implement sleep()
synchronously by spinning (highly discouraged because it will make your site unresponsive) or asynchronously with Promises.
Example of how a sleep()
function can be implemented asynchronously:
function sleep(milli) {
return new Promise(resolve => setTimeout(resolve, milli));
}
Now, to refactor your code to use async
/await
, we mostly only need to inline the setTimeout()
function. Simply put, until the setTimeout()
call we can do as usual, then sleep()
for the specified time (1000 milliseconds), and then inline the callback:
function sleep(milli) {
return new Promise(resolve => setTimeout(resolve, milli));
}
let available = true;
let requested = 0;
async function notTwiceASecond() {
if (available) {
available = false;
console.log("Executed");
// ...
await sleep(1000);
available = true;
if (requested) {
--requested;
notTwiceASecond();
}
} else {
++requested;
}
}
for (let i = 0; i < 3; ++i) {
console.log("called");
notTwiceASecond();
}
Since we want to await
the sleep()
call, we have to declare notTwiceASecond()
as async
.
One important thing I'd like to mention is that I moved available = false
to the top of the if-block. I did this for several reasons:
- Semantically, we have just started working on the call, so we should be unavailable from the beginning.
- Programmatically because if we were to call
notTwiceASecond()
from within the left-out code (// ...
), your function would behave differently from how it should. This bug also exists in your original code.
Refactoring your code like this will change its behaviour with other setTimeout()
calls and Promises, because of how the event loop works.
Here a short example of the differences:
setTimeout(() => {
console.log(3);
}, 0);
Promise.resolve().then(() => {
console.log(2);
});
console.log(1);
Initial 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;
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);
}
Better "initial 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!
Also: Did you notice that my code also has the bug mentioned in "With async
/await
"? The one where calling from within the task would make the code misbehave.
Try fixing it as an exercise!
Regarding your code
Unlike my implementation that uses an array to enqueue each individual "task", you simply increment a counter. Since your notTwiceASecond()
is always only doing the same task, I prefer yours over mine.
What bugs me most about your code is that available
and requested
are declared on the global context. Trying to redeclare similarly named variables, or using them differently from their intended use would cause your code to misbehave.
There are different ways to remove them from the global context. These are the ways I can think of right now:
- Using properties on the function. (Can be hoisted f lazy-initialized and as function declaration.)
- Wrapping the function in an IIFE. (Not hoisted because it is a function expression.)
// As properties on the function. (Is hoisted)
function notTwiceASecond() {
if (!notTwiceASecond.lazyInitialized) {
notTwiceASecond.available = true;
notTwiceASecond.requested = 0;
notTwiceASecond.lazyInitialized = true;
}
// Replace `available` and `requested` to property accessors like above.
// ... (Initial implementation)
}
// Wrapped in IIFE. (Is not hoisted)
const notTwiceASecond = (function() {
let available = true;
let requested = 0;
function notTwiceASecond() { /* ... */ }
return notTwiceASecond;
})();
You inlined the setTimeout()
call while I outsourced it to its own function pauseThenContinue()
. The only reason I did this is to have my code be easier to read.
The biggest difference then in our implementations is how we handle requested
/queue
:
You immediately execute the call if available and only enqueue if unavailable; I always enqueue and only execute if enqueued. What to prefer here is subjective.
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>
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);
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. 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>
本文标签: javascriptDelay function execution if it has been called recentlyStack Overflow
版权声明:本文标题:javascript - Delay function execution if it has been called recently - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://roclinux.cn/p/1744100302a2533468.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论