| /* |
| * 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]; |
| } |
| } |