/* * 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( fn: T, delay?: number, debounce?: boolean ): T & ThrottleController { let currCall; let lastCall = 0; let lastExec = 0; let timer: ReturnType = 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( 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; } /** * Clear throttle. Example see throttle.createOrUpdate. */ export function clear(obj: T, fnAttr: S): void { const fn = obj[fnAttr]; if (fn && (fn as any)[ORIGIN_METHOD]) { // Clear throttle (fn as any).clear && (fn as any).clear(); obj[fnAttr] = (fn as any)[ORIGIN_METHOD]; } }