【前端面试小册】JS-第25节:防抖节流进阶与实战

一、概述

1.1 核心概念

函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。

  • 函数防抖(debounce):某一段时间内只执行一次,在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
  • 函数节流(throttle):间隔时间执行,规定在一个单位时间内,只能触发一次函数

类比理解

  • 防抖:就像电梯门,有人进来就重新计时,直到没人进来才关门
  • 节流:就像水龙头,无论怎么拧,单位时间内流出的水量是固定的

1.2 区别对比

特性 防抖(debounce) 节流(throttle)
执行时机 停止触发后执行 固定时间间隔执行
适用场景 搜索框输入、窗口 resize 滚动事件、鼠标移动
效果 频繁触发只执行最后一次 频繁触发按固定频率执行

二、防抖(debounce)实现

2.1 基础实现

function debounce(fn, delay, immediate) {
    let timer = null;
    return function (...args) {
        let context = this;
        
        // 清除之前的定时器
        if (timer) clearTimeout(timer);
        
        if (immediate) {
            // 立即执行模式
            let doNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, delay);
            if (doNow) {
                fn.apply(context, args);
            }
        } else {
            // 延迟执行模式
            timer = setTimeout(() => {
                fn.apply(context, args);
                timer = null;
            }, delay);
        }
    };
}

2.2 执行流程

graph TD
    A[触发事件] --> B{immediate模式?}
    B -->|是| C{是否有timer?}
    B -->|否| D[清除旧timer]
    C -->|否| E[立即执行]
    C -->|是| F[不执行]
    E --> G[设置新timer]
    F --> G
    D --> H[设置新timer]
    H --> I[延迟后执行]
    G --> J[延迟后清除timer]

2.3 使用示例

// 基础防抖
const debouncedFn = debounce(() => {
    console.log('执行了');
}, 1000);

// 立即执行模式
const debouncedFnImmediate = debounce(() => {
    console.log('立即执行');
}, 1000, true);

// 搜索框示例
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
    console.log('搜索:', e.target.value);
}, 500));

2.4 增强版本(支持取消)

function debounce(fn, delay, immediate) {
    let timer = null;
    
    const debounced = function (...args) {
        let context = this;
        
        if (timer) clearTimeout(timer);
        
        if (immediate) {
            let doNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, delay);
            if (doNow) {
                fn.apply(context, args);
            }
        } else {
            timer = setTimeout(() => {
                fn.apply(context, args);
                timer = null;
            }, delay);
        }
    };
    
    // 取消功能
    debounced.cancel = function() {
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
    };
    
    return debounced;
}

三、节流(throttle)实现

3.1 时间戳版本(立即执行)

// 时间戳立即执行一次
function throttle(fn, time) {
    let pre = 0;
    return function (...args) {
        let now = Date.now();
        if (now - pre > time) {
            fn.apply(this, args);
            pre = now;
        }
    };
}

特点

  • 第一次触发立即执行
  • 停止触发后不再执行

3.2 定时器版本(延迟执行)

// 定时器版本
function throttle(fn, time) {
    let timer = null;
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args);
                timer = null;
            }, time);
        }
    };
}

特点

  • 第一次触发延迟执行
  • 停止触发后还会执行一次

3.3 完整版本(结合两者优点)

function throttle(fn, threshhold, scope) {
    threshhold || (threshhold = 250);
    var last,
        timer;
    return function () {
        var context = scope || this;
        var now = +new Date(),
            args = arguments;
        if (last && now < last + threshhold) {
            // 如果距离上次执行的时间小于设定的时间周期,则放弃执行
            clearTimeout(timer);
            timer = setTimeout(function () {
                last = now;
                fn.apply(context, args);
            }, threshhold);
        } else {
            // 如果时间周期已经过了,则执行函数
            last = now;
            fn.apply(context, args);
        }
    };
}

特点

  • 第一次触发立即执行
  • 停止触发后还会执行一次
  • 结合了时间戳和定时器的优点

3.4 使用示例

// 基础节流
function myFunction() {
    console.log('Function called!');
}

var myThrottledFunction = throttle(myFunction, 1000);
window.addEventListener('resize', myThrottledFunction);

// 滚动事件示例
window.addEventListener('scroll', throttle(() => {
    console.log('滚动中');
}, 200));

四、应用场景

4.1 防抖(debounce)应用场景

搜索框输入

// 输入框,不断输入值时,用防抖来节约请求资源
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
    const keyword = e.target.value;
    // 发送搜索请求
    fetch(`/api/search?q=${keyword}`)
        .then(res => res.json())
        .then(data => {
            // 更新搜索结果
            updateSearchResults(data);
        });
}, 500));

窗口 resize

// window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
window.addEventListener('resize', debounce(() => {
    console.log('窗口大小改变');
    // 重新计算布局
    recalculateLayout();
}, 300));

按钮提交

// 防止用户重复提交
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', debounce(() => {
    submitForm();
}, 1000, true));  // 立即执行,防止重复点击

4.2 节流(throttle)应用场景

鼠标点击

// 鼠标不断点击触发,mousedown(单位时间内只触发一次)
document.addEventListener('mousedown', throttle(() => {
    console.log('鼠标点击');
}, 1000));

滚动事件

// 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断
window.addEventListener('scroll', throttle(() => {
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;
    
    if (scrollTop + windowHeight >= documentHeight - 100) {
        // 加载更多
        loadMore();
    }
}, 200));

鼠标移动

// 鼠标移动事件
document.addEventListener('mousemove', throttle((e) => {
    console.log('鼠标位置:', e.clientX, e.clientY);
}, 100));

五、实际项目应用

5.1 React Hook 版本

import { useRef, useCallback } from 'react';

// 防抖 Hook
function useDebounce(fn, delay) {
    const timerRef = useRef(null);
    
    const debouncedFn = useCallback((...args) => {
        if (timerRef.current) {
            clearTimeout(timerRef.current);
        }
        timerRef.current = setTimeout(() => {
            fn(...args);
        }, delay);
    }, [fn, delay]);
    
    return debouncedFn;
}

// 节流 Hook
function useThrottle(fn, delay) {
    const lastRun = useRef(Date.now());
    
    const throttledFn = useCallback((...args) => {
        if (Date.now() - lastRun.current >= delay) {
            fn(...args);
            lastRun.current = Date.now();
        }
    }, [fn, delay]);
    
    return throttledFn;
}

5.2 Vue 指令版本

// 防抖指令
Vue.directive('debounce', {
    inserted(el, binding) {
        let timer = null;
        el.addEventListener('click', () => {
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                binding.value();
            }, binding.arg || 1000);
        });
    }
});

// 节流指令
Vue.directive('throttle', {
    inserted(el, binding) {
        let lastTime = 0;
        el.addEventListener('click', () => {
            const now = Date.now();
            if (now - lastTime >= (binding.arg || 1000)) {
                binding.value();
                lastTime = now;
            }
        });
    }
});

六、性能优化

6.1 内存泄漏防护

function debounce(fn, delay, immediate) {
    let timer = null;
    
    const debounced = function (...args) {
        // ... 实现代码
    };
    
    // 清理函数
    debounced.cancel = function() {
        if (timer) {
            clearTimeout(timer);
            timer = null;
        }
    };
    
    return debounced;
}

// 使用示例
const debouncedFn = debounce(() => {
    console.log('执行');
}, 1000);

// 组件卸载时清理
// React: useEffect cleanup
// Vue: beforeDestroy
debouncedFn.cancel();

6.2 参数传递优化

// 使用箭头函数保持 this 指向
function debounce(fn, delay) {
    let timer = null;
    return (...args) => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn(...args);
        }, delay);
    };
}

七、面试要点总结

核心知识点

  1. 防抖:停止触发后执行,适用于搜索、resize
  2. 节流:固定频率执行,适用于滚动、鼠标移动
  3. 实现方式:时间戳、定时器、结合版本
  4. 应用场景:根据实际需求选择防抖或节流

常见面试题

Q1: 防抖和节流的区别?

答:

  • 防抖:停止触发后执行,频繁触发只执行最后一次
  • 节流:固定频率执行,频繁触发按固定频率执行

Q2: 如何实现防抖?

答:使用 setTimeout,每次触发清除旧定时器,设置新定时器。支持立即执行模式。

Q3: 如何实现节流?

答:可以使用时间戳(立即执行)或定时器(延迟执行),或结合两者优点。

实战建议

  • ✅ 理解防抖和节流的区别和适用场景
  • ✅ 掌握基础实现和增强版本
  • ✅ 注意内存泄漏问题(提供取消方法)
  • ✅ 在实际项目中合理使用,提升性能
#前端面试##前端##百度##滴滴##阿里#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

11月了,甚至没有一个流程中的😅不知道是不是北森没认真写还是笔试a太少了,九月投到现在甚至只有两个面试。虎牙一面挂得不明不白,字节二面挂,现在看起来滴滴转正也是没戏了的。广东人不想离开广东,想着广州深圳这么大怎么着也能找个养活自己,但是实际情况是,腾讯暑期到现在就没被捞过一次,广东这边字节投了也没反应。中小厂也基本没反应,零一空间面完让我写笔试题,也挂的不明不白,感觉回答也七七八八,笔试也基本写。现在是看着周围的人上岸中大厂,每一天压力是真的大,高考确实我也尽力了,遇上22年广东高考数学爆炸直接平时还能看得上眼的也没了。为了弥补学历差距,我也觉得我努力了,大二暑假开始实习,从小厂到中厂到大厂,我也努力拼了。回想起滴滴刚oc时候的觉得一切都好起来了,怀着激动的心独自坐上前往北京的飞机,原来也只是昙花一现,来这里才发现是误闯天家,确实也学到了很多,但是也明白确实cover不了正职的活,可能真是学习能力不比了92,写文档和组织代码能力还在被mt喷中😭有时候真在想是不是没有这一年的实习,在学校混混日子,毕业了跑滴滴送外卖也心安理得,但是卷过了,是真不甘心了。好在十月和ld申请转base回到了广州继续实习,很难想象在北京我现在会是什么状态。要不是房租还在交着,真想辞了回家睡几天,虽然也没啥颜面回去面对家人的期望。现在还有两个据说有转正hc的实习生面试,虽然也可能是画大饼,但是属于是转正也比不上同学提前实习工资那种。未来会好起来吗?不知道。春招会好起来吗?我觉得难说。想起之前自信得说我一定能找到满意的工作,很想笑了。现在想起来,日常实习的时候真的是最幸福的时光,没有压力,学得到东西,自己赚钱花也心安理得。
无面如何呢:会赢的
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务