防抖|节流|手撕源码|理解原理

103 阅读3分钟

一、防抖

频繁调用一个函数时,只执行最后一次函数调用。如何判断是最后一次,即一个函数调用完后的一段时间内不再调用则为最后一次函数调用。

/**
* 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
* 当事件密集触发时,函数的触发会被频繁的推迟;
* 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
* 功能:基本实现/this和参数绑定/取消/第一次立即执行/获取回调返回值
* @param {*} callback 需要频繁执行的函数
* @param {*} delay 设置延时时间
* @param {*} immediate 设置是否首次立即执行
* @returns {function}
*/
export function debounce(callback, delay, immediate = false){
    // 计时器
    let timer = null
    // 每轮循环首次立即调用 判断
    let isInvoke = false
    const _debounce = function(...args){
        // 通过promise获取函数调用的返回值
        return new Promise((resolve, reject) => {
            try {
                // 在计时器时间内再次调用取消定时
                if(timer) clearTimeout(timer)
                // 每个循环首次立即调用
                if(immediate && !isInvoke){
                    resolve(callback.apply(this, args))
                    isInvoke = true
                }
                // 重新定时
                timer = setTimeout(() => {
                    resolve(callback.apply(this, args))
                    init()
                }, delay)
            } catch (error) {
                reject(error)
            }
        })
    }
    // 用于取消最后一次函数调用
    const cancel = function() {
        clearTimeout(timer)
        init()
    }
    // 每个循环结束需要重制初始化状态
    const init = function() {
        timer = null
        isInvoke = false
    }
    _debounce.cancel = cancel
    return _debounce
}

/**
* 基本实现:频繁调用取最后一次。最后一次如何定义,在某个延迟时间没再调用函数则为最后一次
* 维护一个定时器 如果在定时器内调用函数 则上个定时器取消 重新开始计时
* @param {*} callback 要调用的函数
* @param {*} delay 延迟时间
* @returns
*/
function baseDebounce(callback, delay){
    let timer = null
    return function(...args){
        if(timer) clearTimeout(timer)
        timer = setTimeout(() => {
            callback.apply(this, args)
            timer = null
        }, delay)
    }
}

二、节流

频繁调用一个函数,在一个时间段内只允许调用一次,让函数调用控制在一定的频率内

/**
* 当事件触发时,会立即执行这个事件的响应函数;
* 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
* 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;
* 基本实现/this和参数/是否立即执行/最后一次是否执行/取消/返回值
* @param {*} callback
* @param {*} interval
*/
export function throttle(callback, interval, {leading = true, trailing = false} = {}){
    // 间隔开始时间
    let startTime = 0
    // 最后一次函数调用计时器
    let timer = null
    const _throttle = function(...args){
        return new Promise((resolve, reject) => {
            try {
                // 当前函数调用时间
                const currentTime = new Date().getTime()
                // 如果首次不执行 则当前时间为开始时间 则当前时间一定在间隔内
                if(!leading && startTime === 0) startTime = currentTime
                // 距离间隔结束的时间 也用来定时最后一次执行函数
                const remainTime = interval - (currentTime - startTime)
                // 判断离间隔时间结束还有多久 负数则在间隔时间之外可以调用
                if(remainTime <= 0) {
                    // 如果有定时器则必定不是最后一次调用
                    if(timer){
                        clearTimeout(timer)
                        timer = null
                    }
                    resolve(callback.apply(this, args))
                    // 更新间隔开始时间
                    startTime = currentTime
                    return
                }
                // 最后一次需要执行则需保存每次函数调用 可能为最后一次也可能作为下一个时间间隔开始
                if(trailing){
                    // 取消上一个可能的最后一次函数执行
                    if(timer) clearTimeout(timer)
                    // 设置这次为可能的最后一次函数执行
                    timer = setTimeout(() => {
                        resolve(callback.apply(this, args))
                        // 下一个间隔开始时间
                        startTime = new Date().getTime()
                        timer = null
                    }, remainTime)
                }
            } catch (error) {
                reject(error)
            }
        })
    }
    // 如果开启尾部执行功能则可以取消最后一次
    function cancel(){
        if(trailing && timer) {
            clearTimeout(timer)
        }
        timer = null
        startTime = 0
    }
    _throttle.cancel = cancel
    return _throttle
}

/**
* 基本实现:在某个时间段内,函数只会被调用一次
* 维护一个时间间隔,首次调用为开始时间,间隔内再次调用函数无效
* 时间间隔外首次调用为下一次间隔开始时间
* @param {*} callback
* @param {*} interval
* @returns
*/
function baseThrottle(callback, interval){
    let startTime = 0
    const _throttle = function(...args){
        const currentTime = new Date().getTime()
        const remainTime = interval - (currentTime - startTime)
        if(remainTime <= 0) {
            callback.apply(this, args)
            startTime = currentTime
        }
    }
    return _throttle
}