Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throttle invokes leadingEdge immediately after trailingEdge #3051

Closed
cpnota opened this issue Mar 10, 2017 · 4 comments
Closed

Throttle invokes leadingEdge immediately after trailingEdge #3051

cpnota opened this issue Mar 10, 2017 · 4 comments
Labels

Comments

@cpnota
Copy link

cpnota commented Mar 10, 2017

throttle with default options invokes the function more frequently than expected. It seems very unlikely that this is intended behavior, and it's inconsistent with underscore behavior.

Reproductions steps:

  1. Call a throttled function twice
  2. leadingEdge is invoked
  3. Wait for the throttle period (wait)
  4. trailingEdge is invoked
  5. Call the throttled function again immediately afterwards
  6. leadingEdge is invoked, even though trailingEdge was just invoked

The root cause is fairly obvious, and has nothing to do with setTimeout inconsistencies. shouldInvoke looks like:

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
        timeSinceLastInvoke = time - lastInvokeTime;

    // Either this is the first call, activity has stopped and we're at the
    // trailing edge, the system time has gone backwards and we're treating
    // it as the trailing edge, or we've hit the `maxWait` limit.
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
  }

timeSinceLastCall is the time since debounced was last invoked. Therefore, in the above scenario, on step 5 shouldInvoke returns true, and leadingEdge is called.

Discovered in a unit test that looks something like:

import { expect } from 'chai';
import sinon from 'sinon';
import throttle from 'lodash.throttle';

describe('lodash.throttle', () => {

	let clock;
	let invokeCount;
	let lastInvoked;

	const func = () => {
		invokeCount++;
		lastInvoked = Date.now();
	}

	const waitFor = ms => new Promise(resolve => {
		setTimeout(resolve, ms);
		clock.tick(ms);
	});


	beforeEach(() => {
		clock = sinon.useFakeTimers();
		invokeCount = 0;
		lastInvoked = undefined;
	});

	afterEach(() => clock.restore());

	it('invokes func at most once every 10 seconds', () => {
		const throttlePeriod = 10;
		const throttled = throttle(func, throttlePeriod);

		expect(invokeCount).to.equal(0);

		throttled();
		throttled();

		expect(invokeCount).to.equal(1);

		return waitFor(throttlePeriod).then(() => {
			expect(invokeCount).to.equal(2);

			throttled();

			expect(invokeCount,
				`throttled was called ${invokeCount} in ${Date.now()} ms.
				Current Time: ${Date.now()}, lastInvoked: ${lastInvoked}`
			).to.equal(2);
		});
	});
});

Also played with these to verify:
Lodash throttle fiddle: http://jsfiddle.net/sccqve9m/
Underscore throttle fiddle: http://jsfiddle.net/missinglink/19e2r2we/

@jdalton
Copy link
Member

jdalton commented Mar 10, 2017

The root cause is fairly obvious, and has nothing to do with setTimeout inconsistencies. shouldInvoke looks like:

Nothing in debounce is fairly obvious.
Would you be up for creating a PR?

@cpnota
Copy link
Author

cpnota commented Mar 10, 2017

Nothing in debounce is fairly obvious.

Fair point. Sure, I'll give it a shot.

@jdalton
Copy link
Member

jdalton commented Mar 11, 2017

Moved to #3053.

@lock
Copy link

lock bot commented Dec 27, 2018

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked as resolved and limited conversation to collaborators Dec 27, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Development

No branches or pull requests

2 participants