Skip to content

Commit

Permalink
feat: add debounce throttle
Browse files Browse the repository at this point in the history
  • Loading branch information
fupengl committed Jun 10, 2021
1 parent 940833d commit 9bf0135
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 25 deletions.
19 changes: 19 additions & 0 deletions src/bom/cancelAnimationFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import global from '../global';

/**
* cancelAnimationFrame polyfill
*/
const CAF: typeof cancelAnimationFrame = (function () {
return (
global.cancelAnimationFrame ||
global.webkitCancelAnimationFrame ||
global.mozCancelAnimationFrame ||
global.oCancelAnimationFrame ||
global.msCancelAnimationFrame ||
function (handle) {
clearTimeout(handle);
}
);
})();

export default CAF;
2 changes: 2 additions & 0 deletions src/bom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type {
} from './xhr-upload';
export { default as downloadCanvasImage } from './download-canvas-image';
export { default as downloadFile } from './download-file';
export { default as requestAnimationFrame } from './requestAnimationFrame';
export { default as cancelAnimationFrame } from './cancelAnimationFrame';

export { default as openWindow } from './open-window';
export * from './cookie';
19 changes: 19 additions & 0 deletions src/bom/requestAnimationFrame.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import global from '../global';

/**
* requestAnimationFrame polyfill
*/
const RAF: typeof requestAnimationFrame = (function () {
return (
global.requestAnimationFrame ||
global.webkitRequestAnimationFrame ||
global.mozRequestAnimationFrame ||
global.oRequestAnimationFrame ||
global.msRequestAnimationFrame ||
function (callback) {
return global.setTimeout(callback, 1000 / 60);
}
);
})();

export default RAF;
16 changes: 3 additions & 13 deletions src/bom/window-smooth-scrolling.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import requestAnimationFrame from './requestAnimationFrame';

/**
* 时间内,滚动条平滑滚到指定位置
* @param to
Expand All @@ -15,18 +17,6 @@ function windowSmoothScrolling(to, duration) {
);
}

const _requestAnimFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
// @ts-ignore
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
}
);
})();

if (duration < 0) {
_setScrollTop(to);
return;
Expand All @@ -38,7 +28,7 @@ function windowSmoothScrolling(to, duration) {

const step = (diff / duration) * 10;

_requestAnimFrame(function () {
requestAnimationFrame(function () {
if (Math.abs(step) > Math.abs(diff)) {
_setScrollTop(_getScrollTop() + diff);
}
Expand Down
2 changes: 1 addition & 1 deletion src/function/after.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyFn } from '../type';
import type { AnyFn } from '../type';

/**
* 执行多少次之后,再执行
Expand Down
2 changes: 1 addition & 1 deletion src/function/before.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AnyFn } from '../type';
import type { AnyFn } from '../type';

/**
* 只执行最开始的几次,超过次数之后返回最后一次执行的结果
Expand Down
149 changes: 149 additions & 0 deletions src/function/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import type { AnyFn } from '../type';
import { prefSetTimeout, clearPrefTimeout } from './pref-setTimeout';

export type DebounceOptions = {
leading?: boolean;
maxWait?: number;
trailing?: boolean;
};

export interface DebouncedFunc<T extends AnyFn> {
(...args: Parameters<T>): ReturnType<T> | undefined;
cancel(): void;
flush(): ReturnType<T> | undefined;
pending(): boolean;
}

/**
* debounce
* @param func
* @param wait
* @param options
* @ref https://github.com/lodash/lodash/blob/master/debounce.js
*/
function debounce<T extends AnyFn>(
func: T,
wait?: number,
options?: DebounceOptions,
): DebouncedFunc<T> {
if (typeof func !== 'function') {
throw new TypeError('Expected a function');
}

const { leading = false, maxWait = 0, trailing } = options || {};
let lastArgs, lastThis, result, timerId, lastCallTime;
let lastInvokeTime = 0;
wait = +wait! || 0;
const maxing = Math.max(+maxWait! || 0, wait);

function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

function startTimer(pendingFunc, wait) {
return prefSetTimeout(pendingFunc, wait);
}

function cancelTimer(id) {
clearPrefTimeout(id);
}

function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}

function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait! - timeSinceLastCall;

return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
}

function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;

return (
lastCallTime === undefined ||
timeSinceLastCall >= wait! ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}

function timerExpired() {
const time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = startTimer(timerExpired, remainingWait(time));
}

function trailingEdge(time) {
timerId = undefined;

if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}

function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}

function flush() {
return timerId === undefined ? result : trailingEdge(Date.now());
}

function pending() {
return timerId !== undefined;
}

const debounced: DebouncedFunc<T> = function (this, ...args: any[]) {
const time = Date.now();
const isInvoking = shouldInvoke(time);

lastArgs = args;
lastThis = this;
lastCallTime = time;

if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait);
}
return result;
};

debounced.cancel = cancel;
debounced.flush = flush;
debounced.pending = pending;
return debounced;
}

export default debounce;
4 changes: 4 additions & 0 deletions src/function/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export { default as shallowEqual } from './shallow-equal';
export { default as incrementId } from './increment-id';
export { default as after } from './after';
export { default as before } from './before';
export { prefSetTimeout, clearPrefTimeout } from './pref-setTimeout';
export { prefSetInterval, clearPrefSetInterval } from './pref-setInterval';
export { default as debounce } from './debounce';
export { default as throttle } from './throttle';
41 changes: 41 additions & 0 deletions src/function/pref-setInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import requestAnimationFrame from '../bom/requestAnimationFrame';
import cancelAnimationFrame from '../bom/cancelAnimationFrame';
import incrementId from './increment-id';

const id = incrementId();

const timerIdMap: Record<any, number> = {};

/**
* 优先使用 requestAnimationFrame 实现 setInterval
* @note 当窗口未激活的时候会暂停
* @param handler
* @param ms
* @param args
*/
export function prefSetInterval(handler: Function, ms?: number, ...args: any[]): number {
const _id = id();
const interval = ms || 0;
const startTime = Date.now();
let endTime = startTime;
const loop = () => {
timerIdMap[_id] = requestAnimationFrame(loop);
endTime = Date.now();
if (endTime - startTime >= interval) {
handler(...args);
}
};
timerIdMap[_id] = requestAnimationFrame(loop);
return _id;
}

/**
* 取消 prefSetInterval
* @param handle
*/
export function clearPrefSetInterval(handle: number) {
if (timerIdMap[handle]) {
cancelAnimationFrame(timerIdMap[handle]);
delete timerIdMap[handle];
}
}
42 changes: 42 additions & 0 deletions src/function/pref-setTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import requestAnimationFrame from '../bom/requestAnimationFrame';
import cancelAnimationFrame from '../bom/cancelAnimationFrame';
import incrementId from './increment-id';

const id = incrementId();

const timerIdMap: Record<any, number> = {};

/**
* 优先使用 requestAnimationFrame 实现 setTimeout
* @note 当窗口未激活的时候会暂停
* @param handler
* @param timeout
* @param args
*/
export function prefSetTimeout(handler: Function, timeout?: number, ...args: any[]): number {
const _id = id();
const interval = timeout || 0;
const startTime = Date.now();
let endTime = startTime;
const loop = () => {
timerIdMap[_id] = requestAnimationFrame(loop);
endTime = Date.now();
if (endTime - startTime >= interval) {
handler(...args);
clearPrefTimeout(timerIdMap[_id]);
}
};
timerIdMap[_id] = requestAnimationFrame(loop);
return _id;
}

/**
* 取消 prefSetTimeout
* @param handle
*/
export function clearPrefTimeout(handle: number) {
if (timerIdMap[handle]) {
cancelAnimationFrame(timerIdMap[handle]);
delete timerIdMap[handle];
}
}
19 changes: 19 additions & 0 deletions src/function/throttle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AnyFn } from '../type';
import debounce from './debounce';

export type ThrottleSettings = {
leading?: boolean;
trailing?: boolean;
};

function throttle<T extends AnyFn>(func: T, wait?: number, options?: ThrottleSettings) {
const { leading = true, trailing = true } = options || {};

return debounce<T>(func, wait, {
leading,
trailing,
maxWait: wait,
});
}

export default throttle;
20 changes: 10 additions & 10 deletions src/object/timeout-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,6 @@ class TimeoutMap<K, V> extends Map<K, V> {
}
};

private _cleanExpirationElement() {
for (const [key, arg] of this._keyArgs) {
if (arg.expirationTime && arg.expirationTime < Date.now().valueOf()) {
arg.options?.onTimeout?.(key, super.get(key)!, this._keyArgs.get(key)!, this);
this._keyArgs.delete(key);
this.delete(key);
}
}
}

private _clearTimeout(key: K): boolean {
const arg = this._keyArgs.get(key);
if (arg?.timerId !== undefined) {
Expand All @@ -158,6 +148,16 @@ class TimeoutMap<K, V> extends Map<K, V> {
return false;
}

private _cleanExpirationElement() {
for (const [key, arg] of this._keyArgs) {
if (arg.expirationTime && arg.expirationTime < Date.now().valueOf()) {
arg.options?.onTimeout?.(key, super.get(key)!, this._keyArgs.get(key)!, this);
this._keyArgs.delete(key);
this.delete(key);
}
}
}

private _cleanOverLimitElement() {
// TODO Prioritize the clearing of fast-expiring
if (this._options.maxLength !== undefined && super.size > this._options.maxLength) {
Expand Down

0 comments on commit 9bf0135

Please sign in to comment.