手写防抖节流,前端性能优化的基本功

2 阅读3分钟

输入框搜索建议、滚动事件监听、窗口resize……这些场景如果直接绑定事件处理函数,会频繁触发回调,性能直接拉胯。

解决方案就两个:防抖(debounce)和节流(throttle)。今天手写实现,彻底搞懂。

问题场景

// 搜索框实时联想
input.addEventListener('input', function(e) {
    fetchSuggestions(e.target.value);
    // 用户输入"hello",触发了5次请求
    // h → he → hel → hell → hello
});

// 滚动事件
window.addEventListener('scroll', function() {
    updatePosition();
    // 滚动一屏可能触发几百次
});

防抖(debounce)

防抖的思路:事件触发后等N毫秒,如果期间没有再次触发,才执行。如果期间又触发了,重新计时。

function debounce(fn, delay) {
    let timer = null;

    return function(...args) {
        // 每次触发都清除上一个定时器
        if (timer) clearTimeout(timer);

        // 重新设一个定时器
        timer = setTimeout(() => {
            fn.apply(this, args);
            timer = null;
        }, delay);
    };
}

// 使用
const handleSearch = debounce(function(value) {
    fetchSuggestions(value);
}, 300);

input.addEventListener('input', function(e) {
    handleSearch(e.target.value);
});

工作原理:

用户输入:h   e   l   l   o
触发时间:0  100 200 300 400

debounce(300ms):
  0ms: 设定时器,300ms后执行
  100ms: 清除上一个,重新设
  200ms: 清除上一个,重新设
  300ms: 清除上一个,重新设
  400ms: 清除上一个,重新设
  700ms: 定时器到期,执行!

结果:只执行了1次

节流(throttle)

节流的思路:规定一个时间间隔内,最多只执行一次。

function throttle(fn, interval) {
    let lastTime = 0;

    return function(...args) {
        const now = Date.now();

        if (now - lastTime >= interval) {
            fn.apply(this, args);
            lastTime = now;
        }
    };
}

// 使用
const handleScroll = throttle(function() {
    updatePosition();
}, 200);

window.addEventListener('scroll', handleScroll);

工作原理:

滚动事件:0  50  100 150 200 250 300 350 400

throttle(200ms):
  0ms:   执行!(距上次0ms ≥ 200ms?不,lastTime初始为0,now-0 ≥ 200,执行)
  50ms:  不执行(距上次50ms < 200ms)
  100ms: 不执行
  150ms: 不执行
  200ms: 执行!(距上次200ms ≥ 200ms)
  250ms: 不执行
  300ms: 不执行
  350ms: 不执行
  400ms: 执行!(距上次200ms ≥ 200ms)

结果:执行了3次

防抖 vs 节流

对比防抖(debounce)节流(throttle)
核心思想等停下来再执行固定频率执行
执行频率停止触发后执行1次每N毫秒最多执行1次
适用场景搜索联想、表单验证滚动监听、resize
响应速度慢(要等延迟)快(固定间隔)

进阶版:带立即执行的防抖

有时候希望第一次就执行,后面再防抖:

function debounceImmediate(fn, delay) {
    let timer = null;

    return function(...args) {
        if (timer === null) {
            // 第一次立即执行
            fn.apply(this, args);
        }
        clearTimeout(timer);
        timer = setTimeout(() => {
            timer = null;
        }, delay);
    };
}

进阶版:带尾调用的节流

标准节流可能在持续触发时"饿死"——如果间隔内一直触发,最后一次可能丢失。加上尾调用保证最后一次也能执行:

function throttleWithTail(fn, interval) {
    let lastTime = 0;
    let timer = null;

    return function(...args) {
        const now = Date.now();
        const remaining = interval - (now - lastTime);

        if (remaining <= 0) {
            if (timer) {
                clearTimeout(timer);
                timer = null;
            }
            fn.apply(this, args);
            lastTime = now;
        } else if (!timer) {
            // 设置尾调用
            timer = setTimeout(() => {
                fn.apply(this, args);
                lastTime = Date.now();
                timer = null;
            }, remaining);
        }
    };
}

lodash的用法

实际项目中直接用lodash就好:

import { debounce, throttle } from 'lodash';

// 防抖
const handleSearch = debounce(fetchSuggestions, 300);

// 节流
const handleScroll = throttle(updatePosition, 200);

// 在Vue中使用
export default {
    methods: {
        handleSearch: debounce(function() {
            this.fetchData();
        }, 300)
    },
    beforeDestroy() {
        // 组件销毁时取消
        this.handleSearch.cancel();
    }
}

防抖和节流是前端性能优化的基本功,面试也常考手写。理解了原理,手写出来不难,关键是要分清楚什么场景用哪个。

你们项目中哪些地方用到了防抖节流?