JavaScript——防抖与节流
1 什么是 JS 防抖与节流?
在进行窗口的 resize、scroll以及输入框内容校验等操作时,这些事件对应的处理函数被调用的频率会非常高,浏览器的响应速度可能低于事件的触发频率,从而出现延迟或卡顿现象,降低用户的体验。节流和防抖作为页面性能优化的一种策略,可以降低回调函数的执行频率,节省计算资源,有效减少浏览器引擎的损耗,保证用户的体验。
函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果在设定的时间到来之前,事件再次被触发,则重新设定延时时间。
函数节流(throttle):当持续触发事件时,事件处理函数执行一次,如果在设定的时间到来之前,事件再次被触发,事件处理函数则不会执行,或者在设定的时间段最后一刻执行一次,保证一定时间段内只调用一次事件处理函数。
2 函数防抖(debounce)
2.1 实现方式
每次触发事件时先清除之前的延时调用方法(clearTimeout()),然后设置一个新的延时调用方法(setTimeout())。
2.2 使用场景
- 用户短时间内重复点击按钮提交;(例如:登录、发布任务、点赞与取消点赞等)
- 输入框搜索联想事件;
2.3 缺点
如果事件在规定的时间间隔内不断被触发,则事件处理函数的执行则会不断地延迟。
2.4 函数实现及其演化
2.4.1 简单实现
//简单的防抖函数
function debounce(func, wait) {
//定时器变量
let timeout;
return function () {
//每次触发scroll,先清除定时器
clearTimeout(timeout);
//wait秒后触发事件操作func (setTimeout返回此定时器的编号)
timeout = setTimeout(func, wait);
};
};
//绑定在scroll事件上的handlerFunc
function handlerFunc() {
console.log('Success');
}
//没采用防抖动
window.addEventListener('scroll', handlerFunc);
//采用防抖动
window.addEventListener('scroll', debounce(handlerFunc, 1000));
2.4.2 this 指向、event 绑定问题
//以闭包的形式返回一个函数,内部解决了this指向的问题和event对象传递的问题
function debounce(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
};
}
2.4.3 立即触发问题
//首次触发执行,再次触发以后开始执行防抖函数
//使用时不用重复执行这个函数,本身返回的函数才具有防抖功能
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if(timeout) clearTimeout(timeout);
//是否在某一批事件中首次执行
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
immediate = true;
}, wait);
if (callNow) {
func.apply(context, args);
}
immediate = false;
} else {
timeout = setTimeout(function () {
func.apply(context, args);
immediate = true;
}, wait);
}
}
}
2.4.4 返回值问题
function debounce(func, wait, immediate) {
let timeout, result;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function () {
result = func.apply(context, args);
}, wait);
if (callNow) {
result = func.apply(context, args);
}
} else {
timeout = setTimeout(function () {
result = func.apply(context, args);
}, wait);
}
return result;
}
}
3 函数节流(throttle)
3.1 实现方式
每次触发事件时,如果当前有等待执行的延时函数,则直接 return。
3.2 使用场景
- 调整浏览器窗口大小 resize、页面滚动加载 scroll;
- 图片懒加载;
3.3 函数实现
3.3.1 定时器方式
//首次不会立即执行,最后一次会执行,和时间戳方式互补
function throttle(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
func.apply(context, args);
timeout = null;
}, wait);
}
}
}
3.3.2 时间戳方式
//在开始触发时会立即执行一次,和定时器方式互补
function throttle(func, wait) {
let pre = 0;
return function () {
let context = this;
let args = arguments;
let now = +new Date();
if (now - pre > wait) {
func.apply(context, args);
pre = now;
}
}
}
4 示例代码
4.1 微信小程序示例
-
在根目录下的 utils 包中创建 functionUtil.js
/** * 可以第一次立即触发的防抖函数 * @param {*} fn 待防抖函数 * @param {*} interval 间隔时间 */ export function debounce(fn, interval) { let timer; let gapTime = interval || 1000; //间隔时间,如果未传入interval,则默认1s return function() { clearTimeout(timer); let context = this; let args = arguments; //保存此处的arguments,因为setTimeout是全局的,arguments不是防抖函数需要的 timer = setTimeout(function() { fn.call(context,args); }, gapTime); }; } /** * 节流函数 * @param {*} fn 待节流函数 * @param {*} interval 间隔时间 */ export function throttle(fn, interval) { let enterTime = 0; //触发的时间 let gapTime = interval || 1000 ; //间隔时间,如果未传入interval,则默认1s return function() { let context = this; let backTime = new Date(); //第一次函数return即触发的时间 if (backTime - enterTime > gapTime) { fn.call(context, arguments); enterTime = backTime; //赋值给第一次触发的时间,这样就保存了第二次触发的时间 } }; }
-
在需要调用的 js 文件中引入并调用
import { debounce, throttle } from "../../utils/functionutil"; Page({ data: { }, // 调用防抖函数 functionX: debounce(function() { this.handlerFunc1(); }, 1500), // 调用节流函数 functionY: throttle(function() { this.handlerFunc2(); }, 1500), handlerFunc1() { }, handlerFunc2() { } })