| /* | |
| * 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]) { | |
| // Clear throttle | |
| (fn as any).clear && (fn as any).clear(); | |
| obj[fnAttr] = (fn as any)[ORIGIN_METHOD]; | |
| } | |
| } |