函数防抖(debounce)
- 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时;
- 如下图,持续触发 scroll 事件时,并不执行 handle 函数,当 1000 毫秒内没有触发 scroll 事件时,才会延时触发 scroll 事件;
应用场景
文字输入时的 keyup 事件:输入搜索关键词的时候,进行自动完成或者自动联想,这种情况下用户每敲击一次键盘就会触发一次 keyup 事件,此时用户可能连一个字都没有输入完成,此时可以使用 js 防抖,在用户停止输入的一段时间后 (如 500ms) 触发判断,进行自动联想或者自动搜索;
表单验证;
按钮提交事件;
浏览器的窗口缩放 (resize 事件);
缺点
如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟;
防抖初步实现
当持续触发事件时,事件处理函数 handle 只在停止滚动 1000 毫秒之后才会调用一次,也就是说在持续触发事件的过程中,事件处理函数 handle 一直没有执行;
function debounce(fn, wait, immediate) {
// 记录 setTimeout 的返回值(唯一)
var timeout = null;
return function proxy(ev) {
var _this = this;
clearTimeout(timeout);
if (immediate) {
// 只要一直移动,timeout 则一直有值,则 callNow 为 fasle,就不会立即执行
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) fn.call(_this, ev);
} else {
timeout = setTimeout(function () {
fn.call(_this, ev);
}, wait);
}
}
}
// 处理函数
function handle(ev) {
console.log(Math.random());
console.log(this);
console.log(ev);
}
// leftNode:dom 节点绑定移动事件
leftNode.onmousemove = debounce(handle, 1000, true);
防抖函数中的返回值和取消操作
function debounce(fn, wait, immediate) {
// 记录 setTimeout 的返回值(唯一)
var timeout, result;
var proxy = function (ev) {
var _this = this;
if (timeout) clearTimeout(timeout);
if (immediate) {
// 只要一直移动,timeout 则一直有值,则 callNow 为 fasle,就不会立即执行
var callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) result = fn.call(_this, ev);
} else {
timeout = setTimeout(function () {
fn.call(_this, ev);
}, wait);
}
return result;
}
// 取消操作
proxy.cancel = function () {
clearTimeout(timeout);
timeout = null;
}
return proxy;
}
// 处理函数
function handle(ev) {
console.log(Math.random());
console.log(this);
console.log(ev);
return '想要的结果';
}
var doSome = debounce(handle, 3000, false);
// 取消操作
btn.onclick = function () {
doSome.cancel();
}
// leftNode:dom 节点绑定移动事件
leftNode.onmousemove = doSome;
AbortController 实现
-
优点:
- 简洁性:代码非常简洁,利用了现代浏览器特性;
- 可取消性:通过 AbortController 可以轻松取消待执行的函数;
- 无内存泄漏:不需要管理旧的 定时器 ID;
-
注意事项:
- 此实现依赖于 AbortController,需要现代浏览器或 Node.js 环境;
- 如果需要支持旧浏览器,可以使用传统的定时器实现;
- 防抖时间是从最后一次调用开始计算的;
function debounce(fn, delay) {
let abortController = null;
return function(...args) {
// 取消之前的调用
if (abortController) {
abortController.abort();
}
// 创建新的控制器
abortController = new AbortController();
const signal = abortController.signal;
// 设置定时器
setTimeout(() => {
if (!signal.aborted) {
fn.apply(this, args);
abortController = null;
}
}, delay);
};
}
function debounce(fn, delay, immediate = false) {
let abortController = null;
return function(...args) {
const shouldCallNow = immediate && !abortController;
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
const signal = abortController.signal;
if (shouldCallNow) {
fn.apply(this, args);
}
setTimeout(() => {
if (!signal.aborted && !shouldCallNow) {
fn.apply(this, args);
}
abortController = null;
}, delay);
};
}
函数节流(throttle)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数;
节流通俗解释就比如水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴;
如下图,持续触发 scroll 事件时,并不立即执行 handle 函数,每隔 1000 毫秒才会执行一次 handle 函数;
应用场景
元素拖拽、移动时的 mousemove 事件;
射击游戏;
计算鼠标移动的距离;
监听 scroll 滚动事件;
时间戳实现
当高频事件触发时,第一次会立即触发,而后再怎么频繁地触发事件,也都是每 delay 时间才执行一次,最后一次事件不会被触发;
var throttle = function (func, delay) {
var prev = 0;
return function () {
var now = Date.now();
if (now - prev >= delay) {
func.apply(this, arguments);
prev = Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
// leftNode:dom 节点绑定移动事件
leftNode.onmousemove = throttle(handle, 2000);
定时器实现
当高频事件触发时,第一次不会立即触发,而后再怎么频繁地触发事件,也都是每 delay 时间才执行一次,最后一次事件还会触发;
var throttle = function (func, delay) {
var context, args, timer = null;
return function () {
context = this;
args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
// leftNode:dom 节点绑定移动事件
leftNode.onmousemove = throttle(handle, 2000);
requestAnimationFrame 实现
-
一种基于浏览器刷新率的优化方法,适用于需要频繁触发但又不需要每帧都执行的函数 (如滚动事件、动画等)
-
关键点说明:
- requestAnimationFrame 特性:
- 按浏览器刷新率 (通常 60fps,即约 16.7ms/帧) 执行,天然适合动画相关节流;
- 当页面隐藏时会自动暂停,节省资源;
- 与普通节流的区别:传统时间戳节流 (如 lodash.throttle) 是固定时间间隔,而基于 RAF 的节流与屏幕刷新同步,更适合视觉操作;
- isRequesting 标志的作用:确保在一次 RAF 回调完成前不会重复注册新的 RAF,避免帧间多次执行;
- requestAnimationFrame 特性:
function throttleWithRAF(callback) {
let isRequesting = false;
return function(...args) {
if (!isRequesting) {
isRequesting = true;
requestAnimationFrame(() => {
callback.apply(this, args);
isRequesting = false;
});
}
};
}
// 需要节流的函数
function handleScroll() {
console.log('Scroll event:', window.scrollY);
}
// 使用节流
const throttledScroll = throttleWithRAF(handleScroll);
window.addEventListener('scroll', throttledScroll);
总结
-
函数防抖:将几次操作合并为一次操作进行,原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置,这样一来,只有最后一次操作能被触发;
-
函数节流:使得一定时间内只触发一次函数,原理是通过判断是否到达一定时间来触发函数;
-
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数;函数防抖只是在最后一次事件后才触发一次函数;
第 5️⃣ 座大山:前端性能优化
上一篇