type DebounceOptions = { leading?: boolean; trailing?: boolean; maxWait?: number }
export function debounce<T extends (...args: any[]) => any>(fn: T, wait = 100, options: DebounceOptions = {}) {
let timer: any = null
let lastArgs: any[] | null = null
let lastThis: any
let lastCallTime = 0
let lastInvokeTime = 0
const leading = options.leading === true
const trailing = options.trailing !== false
const maxWait = typeof options.maxWait === 'number' ? options.maxWait : 0
function invoke(time: number) {
const args = lastArgs
const context = lastThis
lastArgs = null
lastThis = null
lastInvokeTime = time
return fn.apply(context, args as any)
}
function startTimer(pending: () => void, ms: number) {
timer = setTimeout(pending, ms)
}
function remainingWait(time: number) {
const sinceLastCall = time - lastCallTime
const sinceLastInvoke = time - lastInvokeTime
const waitTime = wait - sinceLastCall
return maxWait ? Math.min(waitTime, maxWait - sinceLastInvoke) : waitTime
}
function shouldInvoke(time: number) {
const sinceLastCall = time - lastCallTime
const sinceLastInvoke = time - lastInvokeTime
return lastCallTime === 0 || sinceLastCall >= wait || sinceLastCall < 0 || (maxWait && sinceLastInvoke >= maxWait)
}
function trailingInvoke(time: number) {
if (trailing && lastArgs) return invoke(time)
lastArgs = null
lastThis = null
return undefined
}
function debounced(this: any, ...args: any[]) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (!timer) {
if (leading) invoke(time)
startTimer(timerExpired, remainingWait(time))
} else if (maxWait) {
startTimer(timerExpired, remainingWait(time))
}
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
timer = null
trailingInvoke(time)
} else {
startTimer(timerExpired, remainingWait(time))
}
}
;(debounced as any).cancel = () => {
if (timer) clearTimeout(timer)
timer = null
lastArgs = null
lastThis = null
lastCallTime = 0
lastInvokeTime = 0
}
;(debounced as any).flush = () => {
if (timer) {
clearTimeout(timer)
timer = null
return trailingInvoke(Date.now())
}
}
return debounced as T & { cancel: () => void; flush: () => any }
}
节流(Throttle)
ts复制代码
type ThrottleOptions = { leading?: boolean; trailing?: boolean }
export function throttle<T extends (...args: any[]) => any>(fn: T, wait = 100, options: ThrottleOptions = {}) {
let timer: any = null
let lastArgs: any[] | null = null
let lastThis: any
let lastInvoke = 0
const leading = options.leading !== false
const trailing = options.trailing !== false
function invoke(time: number) {
lastInvoke = time
const res = fn.apply(lastThis, lastArgs as any)
lastArgs = null
lastThis = null
return res
}
function trailingInvoke() {
if (trailing && lastArgs) invoke(Date.now())
}
function throttled(this: any, ...args: any[]) {
const time = Date.now()
if (!lastInvoke && !leading) lastInvoke = time
const remaining = wait - (time - lastInvoke)
lastArgs = args
lastThis = this
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer)
timer = null
}
invoke(time)
} else if (!timer && trailing) {
timer = setTimeout(() => {
timer = null
trailingInvoke()
}, remaining)
}
}
;(throttled as any).cancel = () => {
if (timer) clearTimeout(timer)
timer = null
lastArgs = null
lastThis = null
lastInvoke = 0
}
return throttled as T & { cancel: () => void }
}
深拷贝(Deep Clone)
ts复制代码
export function deepClone<T>(input: T, cache = new WeakMap()): T {
if (typeof input !== 'object' || input === null) return input
if (cache.has(input as any)) return cache.get(input as any)
if (input instanceof Date) return new Date(input.getTime()) as any
if (input instanceof RegExp) return new RegExp(input.source, input.flags) as any
if (input instanceof Map) {
const m = new Map()
cache.set(input as any, m as any)
for (const [k, v] of input as any as Map<any, any>) m.set(deepClone(k, cache), deepClone(v, cache))
return m as any
}
if (input instanceof Set) {
const s = new Set()
cache.set(input as any, s as any)
for (const v of input as any as Set<any>) s.add(deepClone(v, cache))
return s as any
}
if (ArrayBuffer.isView(input)) {
const Ctor: any = (input as any).constructor
return new Ctor((input as any))
}
const isArray = Array.isArray(input)
const proto = Object.getPrototypeOf(input as any)
const result: any = isArray ? [] : Object.create(proto)
cache.set(input as any, result)
for (const key of Reflect.ownKeys(input as any)) {
const desc = Object.getOwnPropertyDescriptor(input as any, key)!
if (desc.get || desc.set) Object.defineProperty(result, key, desc)
else result[key as any] = deepClone((input as any)[key as any], cache)
}
return result
}
深比较(Deep Equal)
ts复制代码
export function deepEqual(a: any, b: any, seen = new WeakMap()): boolean {
if (Object.is(a, b)) return true
if (typeof a !== typeof b) return false
if (typeof a !== 'object' || a === null || b === null) return false
if (seen.get(a) === b) return true
seen.set(a, b)
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false
for (const [k, v] of a) if (!b.has(k) || !deepEqual(v, b.get(k), seen)) return false
return true
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false
for (const v of a) if (![...b].some(x => deepEqual(v, x, seen))) return false
return true
}
const keysA = Reflect.ownKeys(a)
const keysB = Reflect.ownKeys(b)
if (keysA.length !== keysB.length) return false
for (const k of keysA) {
const da = Object.getOwnPropertyDescriptor(a, k)
const db = Object.getOwnPropertyDescriptor(b, k)
if (!!da?.get !== !!db?.get || !!da?.set !== !!db?.set) return false
if (da && !da.get && !da.set) if (!deepEqual((a as any)[k as any], (b as any)[k as any], seen)) return false
}
return true
}