/*
|
* Licensed to the Apache Software Foundation (ASF) under one
|
* or more contributor license agreements. See the NOTICE file
|
* distributed with this work for additional information
|
* regarding copyright ownership. The ASF licenses this file
|
* to you under the Apache License, Version 2.0 (the
|
* "License"); you may not use this file except in compliance
|
* with the License. You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing,
|
* software distributed under the License is distributed on an
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* KIND, either express or implied. See the License for the
|
* specific language governing permissions and limitations
|
* under the License.
|
*/
|
|
|
const ORIGIN_METHOD = '\0__throttleOriginMethod' as const;
|
const RATE = '\0__throttleRate' as const;
|
const THROTTLE_TYPE = '\0__throttleType' as const;
|
|
type ThrottleFunction = (this: unknown, ...args: unknown[]) => void;
|
export type ThrottleType = 'fixRate' | 'debounce';
|
|
export interface ThrottleController {
|
clear(): void;
|
debounceNextCall(debounceDelay: number): void;
|
};
|
|
/**
|
* @public
|
* @param {(Function)} fn
|
* @param {number} [delay=0] Unit: ms.
|
* @param {boolean} [debounce=false]
|
* true: If call interval less than `delay`, only the last call works.
|
* false: If call interval less than `delay, call works on fixed rate.
|
* @return {(Function)} throttled fn.
|
*/
|
export function throttle<T extends ThrottleFunction>(
|
fn: T,
|
delay?: number,
|
debounce?: boolean
|
): T & ThrottleController {
|
|
let currCall;
|
let lastCall = 0;
|
let lastExec = 0;
|
let timer: ReturnType<typeof setTimeout> = null;
|
let diff;
|
let scope: unknown;
|
let args: unknown[];
|
let debounceNextCall: number;
|
|
delay = delay || 0;
|
|
function exec(): void {
|
lastExec = (new Date()).getTime();
|
timer = null;
|
fn.apply(scope, args || []);
|
}
|
|
const cb = function (this: unknown, ...cbArgs: unknown[]): void {
|
currCall = (new Date()).getTime();
|
scope = this;
|
args = cbArgs;
|
const thisDelay = debounceNextCall || delay;
|
const thisDebounce = debounceNextCall || debounce;
|
debounceNextCall = null;
|
diff = currCall - (thisDebounce ? lastCall : lastExec) - thisDelay;
|
|
clearTimeout(timer);
|
|
// Here we should make sure that: the `exec` SHOULD NOT be called later
|
// than a new call of `cb`, that is, preserving the command order. Consider
|
// calculating "scale rate" when roaming as an example. When a call of `cb`
|
// happens, either the `exec` is called dierectly, or the call is delayed.
|
// But the delayed call should never be later than next call of `cb`. Under
|
// this assurance, we can simply update view state each time `dispatchAction`
|
// triggered by user roaming, but not need to add extra code to avoid the
|
// state being "rolled-back".
|
if (thisDebounce) {
|
timer = setTimeout(exec, thisDelay);
|
}
|
else {
|
if (diff >= 0) {
|
exec();
|
}
|
else {
|
timer = setTimeout(exec, -diff);
|
}
|
}
|
|
lastCall = currCall;
|
} as T & ThrottleController;
|
|
/**
|
* Clear throttle.
|
* @public
|
*/
|
cb.clear = function (): void {
|
if (timer) {
|
clearTimeout(timer);
|
timer = null;
|
}
|
};
|
|
/**
|
* Enable debounce once.
|
*/
|
cb.debounceNextCall = function (debounceDelay: number): void {
|
debounceNextCall = debounceDelay;
|
};
|
|
return cb;
|
}
|
|
/**
|
* Create throttle method or update throttle rate.
|
*
|
* @example
|
* ComponentView.prototype.render = function () {
|
* ...
|
* throttle.createOrUpdate(
|
* this,
|
* '_dispatchAction',
|
* this.model.get('throttle'),
|
* 'fixRate'
|
* );
|
* };
|
* ComponentView.prototype.remove = function () {
|
* throttle.clear(this, '_dispatchAction');
|
* };
|
* ComponentView.prototype.dispose = function () {
|
* throttle.clear(this, '_dispatchAction');
|
* };
|
*
|
*/
|
export function createOrUpdate<T, S extends keyof T, P = T[S]>(
|
obj: T,
|
fnAttr: S,
|
rate: number,
|
throttleType: ThrottleType
|
): P extends ThrottleFunction ? P & ThrottleController : never {
|
let fn = obj[fnAttr];
|
|
if (!fn) {
|
return;
|
}
|
|
const originFn = (fn as any)[ORIGIN_METHOD] || fn;
|
const lastThrottleType = (fn as any)[THROTTLE_TYPE];
|
const lastRate = (fn as any)[RATE];
|
|
if (lastRate !== rate || lastThrottleType !== throttleType) {
|
if (rate == null || !throttleType) {
|
return (obj[fnAttr] = originFn);
|
}
|
|
fn = obj[fnAttr] = throttle(
|
originFn, rate, throttleType === 'debounce'
|
);
|
(fn as any)[ORIGIN_METHOD] = originFn;
|
(fn as any)[THROTTLE_TYPE] = throttleType;
|
(fn as any)[RATE] = rate;
|
}
|
|
return fn as ReturnType<typeof createOrUpdate>;
|
}
|
|
/**
|
* Clear throttle. Example see throttle.createOrUpdate.
|
*/
|
export function clear<T, S extends keyof T>(obj: T, fnAttr: S): void {
|
const fn = obj[fnAttr];
|
if (fn && (fn as any)[ORIGIN_METHOD]) {
|
obj[fnAttr] = (fn as any)[ORIGIN_METHOD];
|
}
|
}
|