[手写系列]实现debounce和throttle

1,409 阅读4分钟

debounce防抖和throttle节流函数,在开发中经常会用到,今天就试着来手写一下吧~

当然也是面试题整理

手写debounce

参考underscore的函数文档,防抖函数主要实现的功能如下:

返回 function 函数的防反跳版本, 将延迟函数的执行(真正的执行)在函数最后一次调用时刻的 wait 毫秒之后. 对于必须在一些输入(多是一些用户操作)停止到达之后执行的行为有帮助。
所以防抖函数一般会调用时刻 wait 结束之后执行,并且一般只执行一次。

防抖的应用场景可以是:当窗口停止改变大小之后重新计算布局,当滑动到页面底部之后再请求数据(实现懒加载),或者防止连续点击按钮的误触(当然这个也可以通过节流实现)

根据underscore的文档,debounce的参数包括function执行的回调函数, wait调用时刻, 以及布尔类型参数immediate,如果immediatetrue,那么函数在调用后立即执行。此外,还可以调用.cancel()实现取消预定的debounce

那么,让我们开始吧

const myDebounce = function (func, wait, immediate) {
	immediate = immediate || false // immediate默认为false
    let timerId = null // 初始化定时器
    // 因为debounce返回的函数可调用,所以需要返回一个函数
    return function () {
        // 保留函数调用时的this,否则返回函数将指向window
        const context = this 
        // 获得当前函数的传入参数
        const args = arguments
        // 保存函数运行后的结果
        let result
        
        // 如果存在定时器,需要将定时器清除
        if(timerId){ clearTimeout(timerId) }
        if(!immediate){
        // 如果不是立即执行
        	timerId = setTimeout(() =>{
        		result = func.apply(context, args)
        	}, wait)
        }else{
        	let callNow = !timerId
            // 判断是否为初次执行,若timerId为null,为初次执行
            timerId = setTimeout(() =>{
                // 在wait结束时,将timerId置空,保证下次为初次执行状态
                timerId = null
        	}, wait)
            // 通过callNow 实现wait的时间间隔
            if (callNow){ result = func.apply(context, args) }
        }
        return result
    }
}

myDebounce.cancle = function () {
// 取消的实现,就是取消当前的timerId
	clearTimeout(timerId)
    timerId = null // 将闭包的变量置空,防止内存泄漏
}

手写throttle

节流throttle在underscore的函数文档中,有如下的定义:

创建并返回一个像节流阀一样的函数,当重复调用函数的时候,至少每隔wait毫秒调用一次该函数。对于想控制一些触发频率较高的事件有帮助。
所以节流函数的实现方式和debounce还蛮类似的,先来一个时间戳版本

时间戳实现throttle

const myThrottle = function (func, wait) {
	// 之前的时间戳
	let lastTime = 0
	return function fn () {
        const context = this
        const args = arguments
        let result
        
        const now = new Date().valueOf()
        // 每次执行函数时都会比较调用时间now和上一次执行时间lastTime
        if (now - lastTime > wait){
        	result = func.apply(context, args)
            // 重置lastTime
            lastTime = now
        }
        return result
    }
}

时间戳实现方案,在函数初次调用时就会执行,之后间隔wait时段后,再次执行。

在throttle函数中,第三个传入参数options

默认情况下,throttle将在你调用的第一时间尽快执行这个function,并且,如果你在wait周期内调用任意次数的函数,都将尽快的被覆盖。如果你想禁用第一次首先执行的话,传递{leading: false},还有如果你想禁用最后一次执行的话,传递{trailing: false}。
所以,我们的时间戳版本,实现的是`option`为默认值`{leading: true, trailing: false}`状态下的throttle函数

而setTimeout 则可以实现option{leading: false, trailing: true}状态下的throttle函数

定时器实现throttle

const myThrottle = function (func, wait) {
	let timerId = null
	return function fn () {
        const context = this
        const args = arguments
        let result
        clearTimeout(timerId)
        // 当timerId为空时,设置时长为wait的定时器,执行函数
        if (!timerId) {
        	timerId = setTimeout(() => {
        	result = func.apply(context, args)
            // 在函数执行后,将定时器置空,防止再次执行
        	timerId = null
        	}, wait)
        }
        return result
    }
}

结合时间戳和定时器,我们似乎就可以得到,支持optin设置的完整版的throttle

完整版throttle

const myThrottle = function (func, wait, option) {
	const leading = option.leading || true
    const trailing = option.trailing || false
	let timerId = null
    let lastTime = 0
	return function fn () {
        const context = this
        const args = arguments
        let result
        const now = new Date().valeOf()
        // 若leading为false,则初始时间戳作废
        if( !leading && !lastTime){ 
        	lastTime = new Date().valeOf() 
        }
        
        if (now - lastTime > wait){
            result = func.apply(context, args)
            lastTime = now
            // 确保timerId是清空的
            if (timerId) {
            	clearTimeout(timerId)
                timerId = null
            }
        } else if ( trailng && !timerId) {
            // 当 trailing 为 true 时执行
        	clearTimeout(timerId)
        	timerId = setTimeout(() => {
        			result = func.apply(context, args)
        			timerId = null
                    // 更新时间戳
                    lastTime = new Date().valeOf()
        	}, wait)
        }
        
        return result
    }
}
// 实现取消功能
myThrottle.cancle = function () {
	clearTimeout(timerId)
    timerId = null
    lastTime = new Date().valueOf
}