frontend-challenges
frontend-challenges copied to clipboard
432 - Throttle with Leading and Trailing - javascript
index.js
export function throttle(func, wait, options = {}) {
const { leading = true, trailing = true } = options;
let timer = null;
let lastArgs = null;
let lastThis = null;
const later = () => {
if (lastArgs && trailing) {
func.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
timer = setTimeout(later, wait);
} else {
timer = null;
}
};
return function (...args) {
lastArgs = args;
lastThis = this;
if (!timer) {
if (leading) {
func.apply(this, args);
lastArgs = lastThis = null;
}
timer = setTimeout(later, wait);
}
};
}
index.test.js
const { throttle } = require("./index");
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
describe("throttle", () => {
it("calls function immediately when leading is true", () => {
const fn = jest.fn();
const throttled = throttle(fn, 100);
throttled("A");
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("A");
});
it("waits before next call within delay", async () => {
const fn = jest.fn();
const throttled = throttle(fn, 100);
throttled("A");
throttled("B");
expect(fn).toHaveBeenCalledTimes(1);
await wait(120);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenLastCalledWith("B");
});
it("supports disabling leading", async () => {
const fn = jest.fn();
const throttled = throttle(fn, 100, { leading: false });
throttled("A");
expect(fn).not.toHaveBeenCalled();
await wait(120);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("A");
});
it("supports disabling trailing", async () => {
const fn = jest.fn();
const throttled = throttle(fn, 100, { trailing: false });
throttled("A");
throttled("B");
await wait(120);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("A");
});
it("preserves context and arguments", async () => {
const fn = jest.fn(function (x) {
return this.value + x;
});
const obj = { value: 10 };
const throttled = throttle(fn, 100, { leading: true, trailing: true });
throttled.call(obj, 5); // leading
throttled.call(obj, 7); // trailing
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith(5);
expect(fn.mock.instances[0]).toBe(obj);
await wait(120);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenLastCalledWith(7);
expect(fn.mock.instances[1]).toBe(obj);
});
it("does nothing if both leading and trailing are false", async () => {
const fn = jest.fn();
const throttled = throttle(fn, 100, { leading: false, trailing: false });
throttled("A");
throttled("B");
await wait(120);
expect(fn).not.toHaveBeenCalled();
});
it("overrides trailing call with latest arguments", async () => {
const fn = jest.fn();
const throttled = throttle(fn, 100, { leading: true, trailing: true });
throttled("A"); // leading
throttled("B"); // trailing scheduled
throttled("C"); // trailing overrides previous
await wait(120);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenLastCalledWith("C");
});
});