防抖的关键是让同一批连续调用共享一个计时器:新调用取消旧的等待,只保留最后一次。闭包负责保存这个状态;在 Vue 中,还要让每个组件实例拥有自己的防抖函数,并在离开时取消未执行任务。

先看一个闭包

function makeCounter() {
    let count = 0;
    return function () {
        count += 1;
        return count;
    };
}
const first = makeCounter();
const second = makeCounter();
console.log(first(), first(), second()); // 1 2 1

返回的函数仍可访问创建时的词法环境;两次工厂调用产生独立的 count。闭包不是复制当时所有变量的快照,而是保留对相应环境的访问。JavaScript 闭包说明

它也可以用于部分参数固定:

const add = x => y => x + y;
const addTwo = add(2);
console.log(addTwo(5)); // 7

循环中使用 var 时,多个回调可能访问同一个变量;let 的逐次绑定或函数工厂可以隔离状态。选择方式应围绕需要保留的变量,而不是为了“使用闭包”增加套层。

一个可取消的尾触发 debounce

下面保存为 debounce.js,仅实现等待结束后的尾触发,不实现 leading、maxWait 或 Promise 返回值:

export function debounce(fn, delay) {
    if (typeof fn !== "function" ||
        !Number.isFinite(delay) || delay < 0) {
        throw new TypeError("需要函数和非负有限延迟");
    }
    let timer = null;
    function wrapped(...args) {
        const context = this;
        clearTimeout(timer);
        timer = setTimeout(() => {
            timer = null;
            fn.apply(context, args);
        }, delay);
    }
    wrapped.cancel = function () {
        clearTimeout(timer);
        timer = null;
    };
    return wrapped;
}

每次调用 debounce 都建立独立计时器。若在每次 input 事件内部重新调用 debounce,旧定时器就不在同一份状态中,防抖自然失效。delay 为零也只是安排后续定时任务,不是同步执行。

在 Vue 3 中按实例创建

以下使用 Vue 3 的 script setup,示例只更新展示值,方便看清防抖与生命周期关系:

<script setup>
import { ref, watch, onUnmounted } from "vue";
import { debounce } from "./debounce.js";

const query = ref("");
const settledQuery = ref("");
const updateQuery = debounce(value => {
    settledQuery.value = value;
}, 300);

watch(query, value => updateQuery(value));
onUnmounted(() => updateQuery.cancel());
</script>

<template>
  <input v-model="query" aria-label="搜索词">
  <p>等待结束后的输入:{{ settledQuery }}</p>
</template>

不要把这个 wrapper 放在所有组件实例共用的模块级变量里。Vue 2 Options API 可以在 created 中给当前实例创建 wrapper,在 beforeDestroy 中调用 cancel;钩子名不能与 Vue 3 混写。

watchEffect 的依赖不能藏进定时器

watchEffect 只在同步执行阶段追踪依赖。如果仅在 setTimeout 的回调里读取 query.value,不能期待这次异步读取被自动追踪。上面的 watch 显式指定 query,再把值传给防抖函数,关系更清楚。Vue Watchers

如果回调发起请求,取消定时器不会取消已经发出的网络请求。仍需使用请求取消或序号校验,避免旧请求较晚返回覆盖新结果,并处理失败。

自定义指令也需要拥有自己的资源

用指令封装时,每个元素应有独立 wrapper 和监听器引用;挂载时绑定,更新时按约定替换回调,卸载时 removeEventListener 并 cancel。不能在全局共享一个 timer,也不能只创建新的匿名函数用于解绑。

闭包本身不等于内存泄漏。真正要追踪的是:组件结束后,是否还有定时器、事件源或全局对象引用着它的环境。资源清理的完整思路见Vue 生命周期与资源归属。