骚操作:函数防抖与函数节流
JavaScript 中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。
但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就有函数防抖和函数节流。
下面是函数被频繁调用的常见的几个场景:
mousemove 事件。如果要实现一个拖拽功能,需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置 DOM 的位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死。
window.onresize 事件。为 window 对象绑定了 resize 事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果在 window.onresize 事件函数里有一些跟 DOM 节点相关的操作,而跟 DOM 节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。
射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
搜索联想(keyup,keydown,input事件)
监听滚动事件判断是否到页面底部自动加载更多(scroll事件)
比如一个搜索的场景(例如百度),当我在一个文本框中输入文字(键盘按下事件)时,需要将文字发送到服务器,并从服务器得到搜索结果,这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验,类似于下面的效果:
可是如何来实现上面的场景呢?如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的!想想看,我搜索“google”这样的单词,至少需要按 6 次按键,就这一个词,我需要向服务器请求 6 次,并让服务器去搜索 6 次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。
对于这些情况的解决方案就是函数防抖(debounce)或函数节流(throttle),其核心就是限制某一个方法的频繁触发。
函数防抖(debounce)
函数防抖,就是指触发事件后在规定时间内函数只能执行一次,如果在 规定时间内又触发了事件,则会重新计算函数执行时间。
简单的说,当一个动作连续触发,则只执行最后一次。 如,坐公交,司机需要等最后一个人进入才能关门。每次进入一个人,司机就会多等待几秒再关门。
函数节流(throttle)
限制一个函数在规定时间内只能执行一次。 如,乘坐地铁,过闸机时,每个人进入后3秒后门关闭,等待下一个人进入。
竖线的疏密代表事件执行的频繁程度。可以看到,正常情况下,竖线非常密集,函数执行的很频繁。而debounce(函数防抖)则很稀疏,只有当鼠标停止移动时才会执行一次。throttle(函数节流)分布的较为均已,每过一段时间就会执行一次。
常见应用场景 函数节流的应用场景 间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交 函数防抖的应用场景 连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
简单示例
在没有使用函数防抖之前
<body>
<input type="text" id="username">
</body>
<script>
let user = document.getElementById("username");
user.oninput = function(e){
console.log(e.target.value);
}
</script>
每次已输入,就会出现下面的效果:
封装函数防抖函数:
/**
* 函数防抖
* @param {function} func 一段时间后,要调用的函数
* @param {number} wait 等待的时间,单位毫秒
*/
function debounce(func, delay) {
// 设置变量,记录 setTimeout 得到的 id
let timerId = null;
return function (...args) {
if (timerId) {
// 如果有值,说明目前正在等待中,清除它
clearTimeout(timerId);
}
// 重新开始计时
timerId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
再次调用:
user.oninput = debounce((e) => {
console.log(e.target.value);
}, 500);
效果:
效果是立竿见影的
封装函数节流的函数:
const throttle = function (fn, delay) {
let canRun = true,
firstTime = true;
return function (...args) {
if (!canRun) return; // 注意,这里不能用timer来做标记,因为setTimeout会返回一个定时器id
canRun = false;
// 第一次直接执行
if (firstTime) {
fn.apply(this, args)
}
setTimeout(() => {
if (firstTime) {
firstTime = false;
} else {
fn.apply(this, args)
}
canRun = true;
}, delay)
}
}
Comments