Refactor debounce to simplify, reduce timers, fix bugs.

This commit is contained in:
Brandon Wallace
2016-03-07 12:30:30 -06:00
committed by John-David Dalton
parent 092f90d2fc
commit 864e14cb20
2 changed files with 163 additions and 81 deletions

166
lodash.js
View File

@@ -9000,14 +9000,12 @@
* jQuery(window).on('popstate', debounced.cancel); * jQuery(window).on('popstate', debounced.cancel);
*/ */
function debounce(func, wait, options) { function debounce(func, wait, options) {
var args, var lastArgs,
maxTimeoutId, lastThis,
result, result,
stamp, timerId,
thisArg, lastCallTime = 0,
timeoutId, lastInvokeTime = 0,
trailingCall,
lastCalled = 0,
leading = false, leading = false,
maxWait = false, maxWait = false,
trailing = true; trailing = true;
@@ -9022,94 +9020,104 @@
trailing = 'trailing' in options ? !!options.trailing : trailing; trailing = 'trailing' in options ? !!options.trailing : trailing;
} }
function cancel() { function invoke(time) {
if (timeoutId) { var args = lastArgs,
clearTimeout(timeoutId); thisArg = lastThis;
}
if (maxTimeoutId) { lastArgs = lastThis = undefined;
clearTimeout(maxTimeoutId); lastInvokeTime = time;
} result = func.apply(thisArg, args);
lastCalled = 0; return result;
args = maxTimeoutId = thisArg = timeoutId = trailingCall = undefined;
} }
function complete(isCalled, id) { function leadingEdge() {
if (id) { // Reset any `maxWait` timer.
clearTimeout(id); lastInvokeTime = lastCallTime;
// Start the timer to the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invoke(lastCallTime) : result;
}
function trailingEdge(time) {
if (timerId !== undefined) {
clearTimeout(timerId);
timerId = undefined;
} }
maxTimeoutId = timeoutId = trailingCall = undefined; // Only invoke if we have `lastArgs`, which means there has been a call
if (isCalled) { // to `func` since the last invocation
lastCalled = now(); if (trailing && lastArgs) {
result = func.apply(thisArg, args); return invoke(time);
if (!timeoutId && !maxTimeoutId) { }
args = thisArg = undefined; lastArgs = lastThis = undefined;
return result;
}
function checkTimes(time) {
var timeSinceLastInvoke = time - lastInvokeTime,
waitTime = time - lastCallTime;
if (waitTime >= wait) {
// Activity has stopped. We are at the trailing edge.
trailingEdge(time);
return;
}
if (waitTime < 0) {
// The system time has gone backwards. Treat it as the trailing edge.
trailingEdge(time);
return;
}
var shouldInvoke = (maxWait !== false && timeSinceLastInvoke >= maxWait);
// Restart the timer to the smaller of remaining maxWait and remaining wait.
var remainingWait = wait - waitTime;
if (maxWait !== false) {
remainingWait = nativeMin(remainingWait, maxWait - timeSinceLastInvoke);
}
return {
'shouldInvoke': shouldInvoke,
'remainingWait': remainingWait
};
}
function timerExpired() {
var time = now(),
check = checkTimes(time);
timerId = undefined;
if (check !== undefined) {
// Restart the timer.
timerId = setTimeout(timerExpired, check.remainingWait);
if (check.shouldInvoke) {
invoke(time);
} }
} }
} }
function delayed() { function cancel() {
var remaining = wait - (now() - stamp); if (timerId !== undefined) {
if (remaining <= 0 || remaining > wait) { clearTimeout(timerId);
complete(trailingCall, maxTimeoutId);
} else {
timeoutId = setTimeout(delayed, remaining);
} }
lastArgs = lastThis = timerId = undefined;
} }
function flush() { function flush() {
if ((timeoutId && trailingCall) || (maxTimeoutId && trailing)) { return timerId === undefined ? result : trailingEdge(now());
result = func.apply(thisArg, args);
}
cancel();
return result;
}
function maxDelayed() {
complete(trailing, timeoutId);
} }
function debounced() { function debounced() {
args = arguments; lastArgs = arguments;
stamp = now(); lastThis = this;
thisArg = this; lastCallTime = now();
trailingCall = trailing && (timeoutId || !leading);
if (maxWait === false) { if (timerId === undefined) {
var leadingCall = leading && !timeoutId; return leadingEdge();
} else {
if (!lastCalled && !maxTimeoutId && !leading) {
lastCalled = stamp;
}
var remaining = maxWait - (stamp - lastCalled);
var isCalled = (remaining <= 0 || remaining > maxWait) &&
(leading || maxTimeoutId);
if (isCalled) {
if (maxTimeoutId) {
maxTimeoutId = clearTimeout(maxTimeoutId);
}
lastCalled = stamp;
result = func.apply(thisArg, args);
}
else if (!maxTimeoutId) {
maxTimeoutId = setTimeout(maxDelayed, remaining);
}
} }
if (isCalled && timeoutId) { // Check the current times to handle invocations in a tight loop.
timeoutId = clearTimeout(timeoutId); var check = checkTimes(lastCallTime);
} return (check && check.shouldInvoke)
else if (!timeoutId && wait !== maxWait) { ? invoke(lastCallTime)
timeoutId = setTimeout(delayed, wait); : result;
}
if (leadingCall) {
isCalled = true;
result = func.apply(thisArg, args);
}
if (isCalled && !timeoutId && !maxTimeoutId) {
args = thisArg = undefined;
}
return result;
} }
debounced.cancel = cancel; debounced.cancel = cancel;
debounced.flush = flush; debounced.flush = flush;

View File

@@ -4193,6 +4193,76 @@
}, 192); }, 192);
}); });
QUnit.test('should honor leading: false when maxWait is not supplied', function(assert) {
assert.expect(6);
var done = assert.async();
var callCount = 0;
var debounced = _.debounce(function(value) {
++callCount;
return value;
}, 32);
// Leading should not fire.
var actual = [debounced(0), debounced(1), debounced(2)];
assert.deepEqual(actual, [undefined, undefined, undefined]);
assert.strictEqual(callCount, 0);
setTimeout(function() {
// Trailing should fire by now.
assert.strictEqual(callCount, 1);
// Do it again.
var actual = [debounced(4), debounced(5), debounced(6)];
// Previous result.
assert.deepEqual(actual, [2, 2, 2]);
assert.strictEqual(callCount, 1);
}, 128);
setTimeout(function() {
assert.strictEqual(callCount, 2);
done();
}, 256);
});
QUnit.test('should honor leading: false when maxWait is supplied', function(assert) {
assert.expect(6);
var done = assert.async();
var callCount = 0;
var debounced = _.debounce(function(value) {
++callCount;
return value;
}, 32, { 'maxWait': 64 });
// Leading should not fire.
var actual = [debounced(0), debounced(1), debounced(2)];
assert.deepEqual(actual, [undefined, undefined, undefined]);
assert.strictEqual(callCount, 0);
setTimeout(function() {
// Trailing should fire by now.
assert.strictEqual(callCount, 1);
// Do it again.
var actual = [debounced(4), debounced(5), debounced(6)];
// Previous result.
assert.deepEqual(actual, [2, 2, 2]);
assert.strictEqual(callCount, 1);
}, 128);
setTimeout(function() {
assert.strictEqual(callCount, 2);
done();
}, 256);
});
QUnit.test('should invoke the `trailing` call with the correct arguments and `this` binding', function(assert) { QUnit.test('should invoke the `trailing` call with the correct arguments and `this` binding', function(assert) {
assert.expect(2); assert.expect(2);
@@ -21271,7 +21341,7 @@
}); });
QUnit.test('should trigger a second throttled call as soon as possible', function(assert) { QUnit.test('should trigger a second throttled call as soon as possible', function(assert) {
assert.expect(2); assert.expect(3);
var done = assert.async(); var done = assert.async();
@@ -21288,10 +21358,14 @@
throttled(); throttled();
}, 192); }, 192);
setTimeout(function() {
assert.strictEqual(callCount, 1);
}, 254);
setTimeout(function() { setTimeout(function() {
assert.strictEqual(callCount, 2); assert.strictEqual(callCount, 2);
done(); done();
}, 288); }, 384);
}); });
QUnit.test('should apply default options', function(assert) { QUnit.test('should apply default options', function(assert) {