防抖的关键是让同一批连续调用共享一个计时器:新调用取消旧的等待,只保留最后一次。闭包负责保存这个状态;在 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 生命周期与资源归属。