Context: I was writing a simple throttle under the task of a javascript tutorial
Task: write a throttle that works like this:
function f(a) {
console.log(a)
};
// f1000 passes calls to f at maximum once per 1000 ms
let f1000 = throttle(f, 1000);
f1000(1); // shows 1
f1000(2); // (throttling, 1000ms not out yet)
f1000(3); // (throttling, 1000ms not out yet)
// when 1000 ms time out...
// ...outputs 3, intermediate value 2 was ignored
// P.S. Arguments and the context this passed to f1000 should be passed to the original f.
Here's my solution. Strangely, it works fine when I run it step-by-step in a debug console but not otherwise. Any idea why and how to fix it? (I assume it has to do with setTimeout?)
function throttle(f, ms) {
let isCoolDown = true,
queue = []
function wrapper(...args) {
queue.push(args)
if (!isCoolDown) return
isCoolDown = false
setTimeout(function() {
isCoolDown = true
if (queue[0] !== undefined) {
f.apply(this, queue.slice(-1))
queue = []
}
}, ms)
return function() {
f.apply(this, args)
queue = []
}()
}
return wrapper
}
A few things:
1) swap isCoolDown = false and isCoolDown = true
2) you don't need a queue, only one call has to go through the others get discarded with throttling
function throttle(fn, ms) {
let throttle = false;
let timer;
return wrapper(...args) {
if(!throttle) { // first call gets through
fn.apply(this, args);
throttle = true;
} else { // all the others get throttled
if(timer) clearTimeout(timer); // cancel #2
timer = setTimeout(() => {
fn.apply(this, args);
timer = throttle = false;
}, ms);
}
};
}
There is a bug at this line:
f.apply(this, queue.slice(-1))
The .slice method will return an array. Since the args is an array, the result of queue.slice(-1) would be something like:
[ [1, 2, 3] ]
Instead, you can change it to:
f.apply(this, queue.slice(-1)[0])
Related
I wanted to directly call a function (like interrupt handler) when a certain condition is met. I didn't want to using "polling" for that as it increases time complexity.
count = 1
p = new Promise((resolve, reject)=>{
if(count == 2){
resolve("hello")
}
});
p.then((msg)=>{
console.log(msg)
})
console.log("1 now");
count = 2;
I expected console.log(msg) to run when count=2 but this is not the case. It turned out that the promise is still "pending". What is the reason this happens? And how do I implement my question.
You can use a Proxy to listen variable changes.
const count = new Proxy({
value: 0
}, {
set(target, prop, val) {
// set value
target[prop] = val;
console.log(`count is ${val}`);
// stop condition
if (val == 2) {
console.log(`count is 2(trigger stop condition)`);
}
}
});
// wait 2 seconds and change count.value to 1
setTimeout(() => count.value = 1, 2000);
// wait 2 seconds and change count.value to 2
// it triggers the stop condition
setTimeout(() => count.value = 2, 2000);
console.log("Waiting for changes ...");
reference: Listen to js variable change
Proxy is one of the solutions for this. But I post another approach for your case.
You can define a custom class or object, and work with that class. Also you register your custom listener for it, and do whatever.
This is a sample of my code. Maybe it will give you some ideas for your solution.
class MyObject {
constructor(value, func) {
this._value = value;
this._listener = func;
}
get value() {
return this._value;
}
set value(newValue) {
this._listener(newValue);
this._value = newValue;
}
}
function customListener(changedValue) {
console.log(`New Value Detected: ${changedValue}`);
}
const count = new MyObject(1, customListener);
count.value = 2;
The issue you're having is that the code inside the Promise resolves synchronously. It seems like you are assuming Promises are by default async, but that is a common async misconception. So, the code
if(count == 2){
resolve("hello")
}
resolves synchronously (that is, right after you declare count to be 1) so the Promise will never be resolved. If you want to asynchronously check for a condition without using libraries, you can use setInterval:
function checkForCondition(count, time){
return new Promise(resolve => {
const interval = setInterval(() => {
if (count == 2){
resolve("The count is 2!");
}
} , time);
});
}
If you call this function, the callback inside setInterval will be placed on the event loop every x ms, where x is equal to the time parameter.
I am trying to return a function that only invokes a callback function 'func' once per every 'wait' milliseconds.
Additional calls to the callback 'func' within the 'wait' period should NOT be invoked or queued.
This is what I have so far...
function throttle(func, wait) {
function inner(...args) {
setInterval(func(...args), wait);
}
return inner;
}
When I run the code through the test algorithm I get the following errors:
"throttled functions should only be able to be called again after the specified time"
Here is the testing algorithm...
let counter = 0;
const incr = () => counter++;
const throttledIncr = throttle(incr, 32);
throttledIncr();
throttledIncr();
setTimeout(() => {
expect(counter).to.eql(1);
throttledIncr();
setTimeout(() => {
expect(counter).to.eql(2);
done();
}, 32);
}, 32);
"throttled functions return their value"
Here is the testing algorithm...
let counter = 0;
const incr = () => ++counter;
const throttledIncr = throttle(incr, 32);
const result = throttledIncr();
setTimeout(() => {
expect(result).to.eql(1);
expect(counter).to.eql(1);
done();
}, 64);
"throttled functions called repeatedly should adhere to time limitations"
Here is the testing algorithm...
const incr = () => ++counter;
const throttledIncr = throttle(incr, 64);
const results = [];
const saveResult = () => results.push(throttledIncr());
saveResult();
saveResult();
setTimeout(saveResult, 32);
setTimeout(saveResult, 80);
setTimeout(saveResult, 96);
setTimeout(saveResult, 180);
setTimeout(() => {
expect(results[0]).to.eql(1);
expect(results[1]).to.be(undefined);
expect(results[2]).to.be(undefined);
expect(results[3]).to.eql(2);
expect(results[4]).to.be(undefined);
expect(results[5]).to.eql(3);
done();
}, 192);
My questions regarding each case:
How do I prevent the function from being called again ?
Why ISNT my function returning value? I can't deduce what or how to return a value with the given testing algorithm.
What does "throttled functions called repeatedly should adhere to time limitations" even mean? This seems contradictory to the first error. There isn't any mention of setting a time limit so I don't believe using setTimeout here is what they mean...
How do I prevent the function from being called again ?
function throttle(func, wait) {
function inner(...args) {
setInterval(func(...args), wait);
}
return inner;
}
First, your code above does not do what you expect it to do. Currently every time you invoke throttle, you are adding func to the event loop, to be executed on an interval.
So when you call throttleIncr 5 times, you are adding incr to the eventloop to be called five times.
One approach (imo), would be to keep track of the last time that throttle(func) was invoked. The next time throttle(func) is invoked, check to see if the wait time has elapsed. If so, invoke func and save off the new time. If not, return.
Why ISNT my function returning value? I can't deduce what or how to return a value with the given testing algorithm.
Your incr function, IS returning the value, however your throttle function puts it on the eventloop, for asychronous execution, so the return value is not available.
What does "throttled functions called repeatedly should adhere to time limitations" even mean? This seems contradictory to the first error.
This is not a javascript error, and likely a custom failure message from the tests you are invoking.
I tried something here, that seems to be working:
function throttle2(callback, delay = 1000) {
let interval;
let currentArgs;
return (...args) => {
currentArgs = args;
if (!interval) {
interval = setInterval(() => {
if (currentArgs) {
callback(...currentArgs);
currentArgs = null;
} else {
clearInterval(interval);
interval = false;
}
}, delay);
}
};
}
Sandbox: https://codesandbox.io/s/competent-tereshkova-ccop2e?file=/index.js:167-179
This question already has an answer here:
Debounce function that fires FIRST then debounces subsequent actions
(1 answer)
Closed 2 years ago.
I am trying to construct a debounce function definition to give following results. My following code prints the following.
'hi'
'hi'
'hi'
'hi'
I am struggling to figure out how I can leverage interval to contain x as undefined and once this expires again the function returns 'hi'
function debounce(callback, interval) {
return () =>{
let x = callback();
setTimeout(()=>{
x = callback();
},interval);
return x;
}
}
// Expectation running below statements
function giveHi() { return 'hi'; }
const giveHiSometimes = debounce(giveHi, 3000);
console.log(giveHiSometimes()); // -> 'hi'
setTimeout(function() { console.log(giveHiSometimes()); }, 2000); // -> undefined
setTimeout(function() { console.log(giveHiSometimes()); }, 4000); // -> undefined
setTimeout(function() { console.log(giveHiSometimes()); }, 8000); // -> 'hi'
Make a calledRecently flag, which gets set to true when the timeout callback runs. When the timeout callback runs, set another timeout which runs after the interval is up to reset calledRecently to false:
function debounce(callback, interval) {
let calledRecently = false;
let intervalId;
return () => {
const result = calledRecently ? undefined : callback();
calledRecently = true;
clearInterval(intervalId);
intervalId = setTimeout(() => {
calledRecently = false;
}, interval);
return result;
}
}
const giveHiSometimes = debounce(() => 'hi', 3000);
console.log(giveHiSometimes()); // -> 'hi'
setTimeout(function() { console.log(giveHiSometimes()); }, 2000); // -> undefined
setTimeout(function() { console.log(giveHiSometimes()); }, 4000); // -> undefined
setTimeout(function() { console.log(giveHiSometimes()); }, 8000); // -> 'hi'
It sounds like you're after leading edge (immediate) debouncing where an initial event is fired, but batched events after it are debounced.
There are a few ways of writing this, including setTimeout and Date.now but it all comes down to moving a lastAttempt variable of some sort outside of the closure and not re-triggering the callback unless the cooldown time has passed without any attempts to call the function being made. Writing it without setInterval seems cleaner to me.
I'd also add an ...args parameter to the function returned from your leadingDebounce function. This lets you pass parameters into the function if you want.
const leadingDebounce = (fn, cooldown) => {
let lastAttempt = 0;
return (...args) => {
if (Date.now() - lastAttempt > cooldown) {
fn(...args);
}
lastAttempt = Date.now();
};
};
const debounced = leadingDebounce(() => console.log("hi"), 3000);
debounced(); // -> 'hi'
setTimeout(() => console.log(debounced()), 2000); // -> undefined
setTimeout(() => console.log(debounced()), 4000); // -> undefined
setTimeout(debounced, 8000); // -> 'hi'
I would like to implement a helper function like lodash's throttle, but which calls the passed callback only if the last call ended (and if not, delays the new call until then), instead of having a rule like "one call every x milliseconds".
What's the best approach here? Thanks.
So What you eventually want is to queue your function calls and call them from the stack.
// keep a stack of inputs for the function calls
var stack = [],
timer = null;
function process() {
var item = stack.shift();
//
// process the item here
//
if (stack.length === 0) {
clearInterval(timer);
timer = null;
}
}
// every time you call the function on event. instead of calling the processing function
// call this to add it in a queue
function queue(item) {
stack.push(item);
if (timer === null) {
timer = setInterval(process, 500);
}
}
You can make this function work for multiple types of calls too.
// use item as a deciding factor
// keep a stack of inputs for the function calls
var stack = [],
timer = null;
function reset(item){
// reset item here
}
function process() {
var item = stack.shift();
//
// process the item here
//
switch (item.call) {
case 'reset':
reset(item);
break;
case 'stop':
// stop(item);
break;
default:
// code block
// deal with it
}
if (stack.length === 0) {
clearInterval(timer);
timer = null;
}
}
// every time you call the function on event. instead of calling the processing function
// call this to add it in a queue
// for hybrid calls
// use item = {'call': 'reset', 'data': item}
function queue(item) {
stack.push(item);
if (timer === null) {
timer = setInterval(process, 500);
}
}
try using boolean variable like
var running = false;
use setInterval or similar function and put condition if(!running){} for statements to be performed. if statement is going to run then make running = true.
Finally worked out a way :
class Scheduler {
private readonly stack: Function[] = [];
private enqueue(task: Function) {
if (this.stack.length < 2) this.stack.push(task);
else this.stack[1] = task;
}
private dequeue() {
if (this.stack.length) {
const task = this.stack.shift()!;
task();
defer(() => this.dequeue());
}
}
run(task: Function) {
this.enqueue(task);
defer(() => this.dequeue());
}
}
// Usage :
const scheduler = new Scheduler()
// Very frequent call :
scheduler.run(() => { /* your heavy task */ })
I'm trying to create a custom debounce function:
const debounced = [];
const cancelFunc = timeout => () => {
clearTimeout(timeout);
};
function debounce(fn, wait, ...args) {
let d = debounced.find(({ func }) => func === fn);
if (d) {
d.cancel();
} else {
d = {};
debounced.push(d);
}
d.func = fn;
d.timeout = setTimeout(fn, wait, ...args);
d.cancel = cancelFunc(d.timeout);
}
If I use with a named function, it works as intended:
debounce(foo, 1000); // called once with 5 clicks in 1 second
But I can't get it to work with anonymous functions:
debounce(() => { foo(5); }, 1000); // called 5 times with 5 clicks in 1 second
I created a pen here: https://codepen.io/anon/pen/gQvMdR?editors=1011
This happens because of your find condition. Let's back up, and consider this bit of code:
if (
(function(){ return 1 }) === (function(){ return 1 })
) {
console.log('The functions are equal');
} else {
console.log('The functions are NOT equal');
}
// logs 'The functions are NOT equal'
Even though I wrote two identical anonymous functions, they are not strictly equal to each other. When you pass in that anonymous function, that is essentially what you are doing. So, when you search for your array for a previously found function, it will never find a match, because each time debounce(() => { foo(5); }, 1000); is called it creates a new function. Since it'll never find a match, it will never be canceled.
As mentioned by #SLaks "Each call creates a separate function, so you won't find it in the array."
So you just need to store something in the array to match it to, you can use .toString()
// ================
const debounced = [];
const cancelFunc = timeout => () => {
clearTimeout(timeout);
};
function debounce(fn, wait, ...args) {
let d = debounced.find(({ funcString }) => funcString === fn.toString());
if (d) {
d.cancel();
} else {
d = {};
debounced.push(d);
}
d.func = fn;
d.funcString = fn.toString()
d.timeout = setTimeout(fn, wait, ...args);
d.cancel = cancelFunc(d.timeout);
}
// ================
function foo(value) {
console.log('value:', value)
}
function onClickBroken() {
debounce(() => { foo(5); }, 1000);
}
<button onClick="onClickBroken()">Click me 5 times</button>