JS手写题-防抖-节流

22,812 阅读11分钟

防抖

什么是防抖(debounce)?

  • 当事件触发时,相应的函数不会被立即触发,而是会被推迟执行
  • 当事件连续密集触发时,函数的触发会被一直推迟。
  • 只有当等待一段时间后也没有再次触发该事件,那么才会真正执行响应函数
  • 简而言之,防抖就是将函数的执行延迟一定时间,如果在该时间内重新触发事件,那么延迟的时间会重置,只有真正达到延迟时间,才会执行回调函数。
  • 举个例子:游戏中的回城就可以认为是防抖,在回城的读秒过程中,如果再次执行回城操作,那么会重新进行读秒,只有整个读秒过程都没有再次执行回城操作,那么等到读秒结束才能成功回城。

防抖的应用场景

  • 输入框频繁输入内容,搜索或者提交信息。
  • 频繁点击按钮,触发某个事件。
  • 监听浏览器滚动事件。
  • 监听用户缩放浏览器resize事件。

防抖函数案例

  • 监听搜索框的oninput事件,那么每输入一个字符就会执行一次回调函数,假如我们在回调函数中进行网络请求,那么用户输入内容时会发送很多次网络请求,这是非常浪费性能的。

  • 没有使用防抖函数的效果 搜索框.gif

  • 使用了防抖函数的效果

防抖搜索框.gif

  • 可以看到使用了防抖函数,当频繁触发同一个事件时,只有最后一次会被执行。

实现防抖函数

简易版防抖函数

// 第一个参数是需要进行防抖处理的函数,第二个参数是延迟时间,默认为1秒钟
function debounce(fn, delay = 1000) {
// 实现防抖函数的核心是使用setTimeout
    // time变量用于保存setTimeout返回的Id
    
    let time = null
    
    function _debounce() {
        // 如果time不为0,也就是说有定时器存在,将该定时器清除
        if (time !== null) {
            clearTimeout(time)
        }
        
        time = setTimeout(() => {
            fn()
        }, delay)
    }
    
    // 防抖函数会返回另一个函数,该函数才是真正被调用的函数
    return _debounce
}
  • 上面防抖函数的实现,已经基本上可以实现防抖的效果,但是还是会有一点小问题,比如说this的指向和原来的函数是不一致的以及参数问题。

防抖函数的实现-this和参数问题

// 第一个参数是需要进行防抖处理的函数,第二个参数是延迟时间,默认为1秒钟
function debounce(fn, delay = 1000) {
// 实现防抖函数的核心是使用setTimeout
    // time变量用于保存setTimeout返回的Id
    
    let time = null
    
    // 将回调接收的参数保存到args数组中
    function _debounce(...args) {
        // 如果time不为0,也就是说有定时器存在,将该定时器清除
        if (time !== null) {
            clearTimeout(time)
        }
        
        time = setTimeout(() => {
            // 使用apply改变fn的this,同时将参数传递给fn
            fn.apply(this, args)  
        }, delay)
    }
    
    // 防抖函数会返回另一个函数,该函数才是真正被调用的函数
    return _debounce
}
  • 当前我们实现的防抖函数已经基本没有什么问题了,不过如果我们想要让事件第一次触发时,函数会立即执行,而之后的依旧和之前一样。效果如下图所示

防抖搜索框(立即执行).gif

  • 要实现该功能我们应该是让用户传递一个参数,来决定是否第一次触发要执行。

防抖函数的实现-第一次立即触发

// 第一个参数是需要进行防抖处理的函数,第二个参数是延迟时间,默认为1秒钟
// 这里多传一个参数,immediate用来决定是否要第一次立即执行, 默认为false
function debounce(fn, delay = 1000, immediate = false) {
// 实现防抖函数的核心是使用setTimeout
    // time变量用于保存setTimeout返回的Id
    let time = null
    // isImmediateInvoke变量用来记录是否立即执行, 默认为false
    let isImmediateInvoke = false
    
    // 将回调接收的参数保存到args数组中
    function _debounce(...args) {
        // 如果time不为0,也就是说有定时器存在,将该定时器清除
        if (time !== null) {
            clearTimeout(time)
        }
        
        // 当是第一次触发,并且需要触发第一次事件
        if (!isImmediateInvoke && immediate) {
            fn.apply(this, args)
            // 将isImmediateInvoke设置为true,这样不会影响到后面频繁触发的函数调用
            isImmediateInvoke = true;
        }
        
        time = setTimeout(() => {
            // 使用apply改变fn的this,同时将参数传递给fn
            fn.apply(this, args)  
            // 当定时器里的函数执行时,也就是说是频繁触发事件的最后一次事件
            // 将isImmediateInvoke设置为false,这样下一次的第一次触发事件才能被立即执行
            isImmediateInvoke = false
        }, delay)
    }
    
    // 防抖函数会返回另一个函数,该函数才是真正被调用的函数
    return _debounce
}
  • 如果我们需要获得原来函数的返回值,那么我们上面的实现是无法做到的。虽然很少会需要获取返回值,但是这里还是实现一下。
  • 如果我们想要获取到返回值,可以让用户传递一个回调函数,这样可以在执行完函数之后,将返回值以参数的形式传递给回调函数。
  • 也可以通过返回一个Promise的形式,将参数传递出去。
  • 这里采用回调函数的方法实现

防抖函数的实现-返回值问题

// 第一个参数是需要进行防抖处理的函数,第二个参数是延迟时间,默认为1秒钟
// 这里多传一个参数,immediate用来决定是否要第一次立即执行, 默认为false
function debounce(fn, delay = 1000, immediate = false, resultCb) {
// 实现防抖函数的核心是使用setTimeout
    // time变量用于保存setTimeout返回的Id
    let time = null
    // isImmediateInvoke变量用来记录是否立即执行, 默认为false
    let isImmediateInvoke = false
    
    // 将回调接收的参数保存到args数组中
    function _debounce(...args) {
        // 如果time不为0,也就是说有定时器存在,将该定时器清除
        if (time !== null) {
            clearTimeout(time)
        }
        
        // 当是第一次触发,并且需要触发第一次事件
        if (!isImmediateInvoke && immediate) {
            // 将函数的返回值保存到result中
            const result = fn.apply(this, args)
            if (typeof resultCb === 'function') {
                // 当用户传递了resultCb函数时,执行该函数,并将结果以参数传递出去。
                resultCb(result)
            }
            // 将isImmediateInvoke设置为true,这样不会影响到后面频繁触发的函数调用
            isImmediateInvoke = true;
        }
        
        time = setTimeout(() => {
            // 使用apply改变fn的this,同时将参数传递给fn
            fn.apply(this, args)  
            // 当定时器里的函数执行时,也就是说是频繁触发事件的最后一次事件
            // 将isImmediateInvoke设置为false,这样下一次的第一次触发事件才能被立即执行
            isImmediateInvoke = false
        }, delay)
    }
    
    // 防抖函数会返回另一个函数,该函数才是真正被调用的函数
    return _debounce
}

节流

什么是节流

  • 节流是指当事件触发时,会执行这个事件的响应函数。
  • 但是该事件如果被频繁触发,那么节流函数会按照一定的频率来执行函数
  • 节流类似于技能cd,不管你按了多少次,必须等到cd结束后才能释放技能。也就是说在如果在cd时间段,不管你触发了几次事件,只会执行一次。只有当下一次cd转换,才会再次执行。

使用节流函数的效果图

节流函数效果.gif

实现节流函数

// interval 间隔时间,也就是cd的长短
function throttle(fn, interval) {
    //该变量用于记录上一次函数的执行事件
    let lastTime = 0
    
    const _throttle = function(...args) {
        // 获取当前时间
        const nowTime = new Date().getTime()
        
        // cd剩余时间
        const remainTime = nowTime - lastTime
        // 如果剩余时间大于间隔时间,也就是说可以再次执行函数
        if (remainTime - interval >= 0) {
            fn.apply(this, args)
            // 将上一次函数执行的时间设置为nowTime,这样下次才能重新进入cd
            lastTime = nowTime
        }
    }
    // 返回_throttle函数
    return _throttle
}
  • 上面的代码就是基本的节流函数实现,但是会发现第一次事件的响应函数会立即执行。这是因为我们的lastTime一开始设置为0,而当我们触发事件时,获取到的nowTime是一个非常大的值,那么减去lastTime就会大于interval,所以第一次会立即执行。

实现控制第一次函数是否立即执行的节流函数

// leading参数用来控制是否第一次立即执行,默认为true
function throttle(fn, interval, leading = true) {
    //该变量用于记录上一次函数的执行事件
    let lastTime = 0
    // 内部的控制是否立即执行的变量
    let isLeading = true
    
    const _throttle = function(...args) {
        // 获取当前时间
        const nowTime = new Date().getTime()
        
        // 第一次不需要立即执行
        if (!leading && isLeading) {
            // 将lastTime设置为nowTime,这样就不会导致第一次时remainTime大于interval
            lastTime = nowTime
            // 将isLeading设置为false,这样就才不会对后续的lastTime产生影响。
            isLeading = false
        }
        
        // cd剩余时间
        const remainTime = nowTime - lastTime
        // 如果剩余时间大于间隔时间,也就是说可以再次执行函数
        if (remainTime - interval >= 0) {
            fn.apply(this, args)
            // 将上一次函数执行的时间设置为nowTime,这样下次才能重新进入cd
            lastTime = nowTime
        }
    }
    // 返回_throttle函数
    return _throttle
}
  • 实现效果

节流函数实现leading效果.gif

  • 可以发现第一次函数不会立即执行了,但是当我们进行下一轮的点击时下一轮的第一次事件又会立即执行了
  • 这是因为当下一轮事件触发时,isLeading已经被我们设置为false了,所以不会将lastTime设置为nowTime
  • 想要解决这个问题,就得在一轮事件的最后一个执行函数,将isLeading改为true,这样下一轮事件开始时,isLeading就为true,就能够解决上面的问题。
  • 需要使用到定时器,当剩余时间小于间隔时间时,设置一个定时器,定时器的延迟时间为interval,在定时器内将isLeading该为true,
// leading参数用来控制是否第一次立即执行,默认为true
function throttle(fn, interval, leading = true) {
    //该变量用于记录上一次函数的执行事件
    let lastTime = 0
    // 内部的控制是否立即执行的变量
    let isLeading = true
    // time 保存定时器的id
    let time = null
    
    const _throttle = function(...args) {
        // 获取当前时间
        const nowTime = new Date().getTime()
        
        // 第一次不需要立即执行
        if (!leading && isLeading) {
            // 将lastTime设置为nowTime,这样就不会导致第一次时remainTime大于interval
            lastTime = nowTime
            // 将isLeading设置为false,这样就才不会对后续的lastTime产生影响。
            isLeading = false
        }
        
        // cd剩余时间
        const remainTime = nowTime - lastTime
        // 如果剩余时间大于间隔时间,也就是说可以再次执行函数
        if (remainTime - interval >= 0) {
            fn.apply(this, args)
            // 将上一次函数执行的时间设置为nowTime,这样下次才能重新进入cd
            lastTime = nowTime
        }
        
        if (remainTime < interval) {
            // 判断是否存在定时器,如果存在则取消掉
            if (time) clearTimeout(time)
            
            // 设置定时器
            time = setTimeout(() =>{
                // 由于该定时器,会在没有事件触发的interval时间间隔后才会执行,也就是说一轮事件
                // 执行已经结束,使用需要将isLeading复原,这样下一轮事件的第一次事件就不会立即执行了。
                isLeading = true
            }, interval)
        }
    }
    // 返回_throttle函数
    return _throttle
}
  • 上面实现的节流函数已经可以实现控制第一次事件是否立即执行了,但是还是不够。如果我们想要一轮点击事件中的最后一次事件绑定函数能够执行,那么我们目前实现的节流函数是没办法实现这个功能的。
  • 其实在已经实现的代码中,想要实现最后一次事件绑定函数能够执行,是非常简单的,我们只需要让用户传递一个参数,用来控制最以后一次是否执行,然后在定时器内做一下判断即可实现。
// trailing参数用来控制是否最后一次是否执行,默认为false
function throttle(fn, interval, leading = true, trailing = false) {
    //该变量用于记录上一次函数的执行事件
    let lastTime = 0
    // 内部的控制是否立即执行的变量
    let isLeading = true
    // time 保存定时器的id
    let time = null
    
    const _throttle = function(...args) {
        // 获取当前时间
        const nowTime = new Date().getTime()
        
        // 第一次不需要立即执行
        if (!leading && isLeading) {
            // 将lastTime设置为nowTime,这样就不会导致第一次时remainTime大于interval
            lastTime = nowTime
            // 将isLeading设置为false,这样就才不会对后续的lastTime产生影响。
            isLeading = false
        }
        
        // cd剩余时间
        const remainTime = nowTime - lastTime
        // 如果剩余时间大于间隔时间,也就是说可以再次执行函数
        if (remainTime - interval >= 0) {
            fn.apply(this, args)
            // 将上一次函数执行的时间设置为nowTime,这样下次才能重新进入cd
            lastTime = nowTime
        }
        
        if (remainTime < interval) {
            // 判断是否存在定时器,如果存在则取消掉
            if (time) clearTimeout(time)
            
            // 设置定时器
            time = setTimeout(() =>{
                // 判断是否最后一次需要执行
                if (trailing) {
                // 最后一次需要执行
                    fn.apply(this, args)
                }
                // 由于该定时器,会在没有事件触发的interval时间间隔后才会执行,也就是说一轮事件
                // 执行已经结束,使用需要将isLeading复原,这样下一轮事件的第一次事件就不会立即执行了。
                isLeading = true
            }, interval)
        }
    }
    // 返回_throttle函数
    return _throttle
}

节流函数完整实现.gif

  • 经过不断的修改,目前实现的节流函数,已经是比较完善的,如果想要获取返回值,可以参考防抖函数的实现方式。

其他手写函数以及代码

js手写题-常见数组方法

js手写题代码