在进行窗口的resize、scroll、mousemove,输入框内容校验等操作时,如果这些高频事件的处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。
此时可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
假设有一个鼠标移动计数事件。
<div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
<script>
let num = 1;
let content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
</script>
在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。
函数防抖是指触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
防抖的实现上有两种方式:非立即执行、立即执行。
触发高频事件后过n秒执行函数fn,如果n秒内高频事件再次被触发,则重新计算时间
function debounce1(fn, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;//获取this和参数
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(()=>{
fn.apply(context, args); //可以再次触发
}, wait);
}
}
使用上述绑定 mousemove 事件的例子,通过上面的防抖函数,可以这么使用:
content.onmousemove = debounce(count,1000);
触发高频事件后立即执行函数fn,过n秒后触发高频事件可以立即执行函数fn,如果n秒内高频事件再次被触发,则重新计算时间
function debounce2(func,wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
/**
** @desc 函数防抖
** @param fn 函数
** @param wait 延迟执行毫秒数
** @param immediate true 表立即执行,false 表非立即执行
*/
function debounce(fn, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
if (immediate) {
var callnow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callnow) {
fn.apply(context, args);
console.log("可以再次触发了");
}
} else {
timeout = setTimeout(function () {
fn.apply(context, args);
console.log("可以再次触发了");
}, wait);
}
}
}
当连续触发事件时,在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
函数节流也有两种实现方式:时间戳版、定时器版
在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。
function throttle1(fn, wait) {
let previous = 0;
return function () {
let context = this;
let args = arguments;
let now = new Date();
if (now - previous > wait) {
fn.apply(context, args);
previous = now;
}
}
}
使用方法如下:
content.onmousemove = throttle1(count,1000);
在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。
function throttle2(fn, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
fn.apply(context, args);
}, wait);
}
}
}
/**
* @desc 函数节流
* @param fn 函数
* @param wait 延迟执行毫秒数
* @param type "timestamp" 表时间戳版,"timeout" 表定时器版
*/
function throttle(fn, wait, type) {
if (type === "timestamp") {
var previous = 0;
} else if (type === "timeout") {
var timeout;
}
return function () {
let context = this;
let args = arguments;
if (type === "timestamp") {
let now = Date.now();
if (now - previous > wait) {
fn.apply(context, args);
previous = now;
}
} else if (type === "timeout") {
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
fn.apply(context, args)
}, wait)
}
}
}
}
如果事件触发频繁,防抖中的计时会不断重置,从而会不断延迟了函数的执行,而节流中的计时器不会因为事件的触发而重置。
在表单提交这一场景中,可以考虑使用立即执行版的防抖。
用户往往会因为提交时的等待而重复快速点击按钮进行提交表单。显然,这一过程中,表单的内容不会发生改变,我们只需要调用一次后台接口。
使用立即执行版的防抖之后,会在用户第一次点击按钮时调用一次后台接口,如果用户在延迟时间内重复点击,则不会执行表单提交函数,直至用户最后一次点击按钮并等待超过设定的延迟时间之后再次点击按钮,才会执行表单提交函数。
在滚动切换导航栏项的场景中,需要先获取各个元素的位置offsetTop,监听浏览器窗口滚动事件,触发该事件时,获取滚动条的位置scrollTop,遍历offsetTop与scrollTop进行比较,从而确定当前位置需要高亮哪个导航栏项。
在多次滚动事件触发的过程中,执行多次函数,因此,我们可以考虑使用时间戳版的节流(定时器版也可)。
减少函数的执行频率,防止因大量计算导致卡顿。
参考文章:
https://www.jianshu.com/p/c8b86b09daf0
https://www.jianshu.com/p/72cf774b41c8