函数的防抖
介绍
函数防抖中的抖动就是执行的意思,而一般的抖动都是持续的、多次的、频繁的执行某一段代码。函数防抖就是某函数持续多次执行,我们希望让它冷静下来再执行。也就是当持续触发事件的时候,函数是完全不执行的,等最后一次触发结束的一段时间之后,再去执行。在前端开发中经常会遇到这种频繁的事件触发,比如:
- window 的 resize、scroll
- mousedown、mousemove
- keyup、keydown 、等等…
观察以下示例代码来了解事件如何频繁的触发:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
<title>debounce</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var count = 1;
var container = document.getElementById('container');
function getUserAction() {
container.innerHTML = count++;
};
// 鼠标移动时触发指定函数getUserAction
container.onmousemove = getUserAction;
</script>
</body>
</html>
上面的案例效果如图所示:
解释:上面简单例子中鼠标从左边滑到右边就触发了 165 次 getUserAction 函数,若是复杂的回调函数或是网络请求请求,在 1 秒触发了 60 次,那么每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。严重的影响了用户体验与应用的性能。为了解决这个问题,一般有两种解决方案:
- 函数的防抖(debounce)
- 函数的节流(throttle)
概念
函数防抖和节流,都是控制事件触发频率的方法。其中防抖的原理就是:事件尽管触发,但是在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发相同事件,那我就以新的事件 n 秒后才执行,舍弃掉上一次事件触发执行操作。简单地说就是频繁的触发事件完毕后的 n 秒内不再触发同一事件,函数才会执行。
函数防抖的实现
实现:根据函数防抖的原理,如果需要使用js代码实现需要 setTimeout()
与 clearTimeout()
配合使用。实现方法如下:
// debounce 接受两个参数 func 需要防抖的函数, wait 事件出发完毕后延迟时间
function debounce(func, wait) {
var timeout; // 使用闭包持久化延时器 id 变量
return function () {
clearTimeout(timeout) // 每次抖动执行函数时清楚上一次的延迟函数,从而达到取消上一次函数执行的效果
timeout = setTimeout(func, wait);
}
}
container.onmousemove = debounce(getUserAction, 1000);
思考:上面的debounce函数虽然实现了函数的防抖效果,但是他会影响到防抖函数 this 指向问题,因为getUserAction()
由 setTimeout()
触发,导致 getUserAction()
函数内部 this 指向 window 对象。解决 this 指向问题可以通过bind()
、 call()
或apply()
方法修改:
function debounce(func, wait) {
var timeout;
return function () {
// 返回的函数会作为事件处理函数绑定给对应的监听事件,此时的this指向的是正确的对象
var context = this;
// 使用bind创建一个指定this指向为当前对象的新函数并替换原函数
func = func.bind(context)
// 每次抖动执行函数时清楚上一次的延迟函数,从而达到取消上一次函数执行的效果
clearTimeout(timeout)
// 此时 func 已经是被指定了this的新函数
timeout = setTimeout(func, wait);
}
}
container.onmousemove = debounce(getUserAction, 1000);
思考:虽然上面的代码我们解决了this的指向问题,但是绑定的事件回调函数会默认接受 event
事件对象作为参数,提供给开发人员处理相应的操作。所以我们还要对上面的代码进行如下改进,使得防抖函数支持 event
默认参数:
function debounce(func, wait) {
var timeout;
// 返回的函数会作为事件处理函数绑定给对应的监听事件,此时函数会默认接受event事件对象作为参数
return function (event) {
var context = this;
// 将 event 事件对象作通过bind方法传递给防抖函数作为参数。
func = func.bind(context, event)
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
container.onmousemove = debounce(getUserAction, 1000);
注意:上面的代码显式的获取event事件对象并传递到绑定this的函数中我们可以使用 arguments 解决这一问题
function debounce(func, wait) {
var timeout;
return function (event) {
var context = this;
var args = arguments;
func = func.bind(context, args)
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
container.onmousemove = debounce(getUserAction, 1000);
函数防抖实现后我们再观察页面效果:
「课堂练习」
请对表单元素的用户输入实现防抖
要求:
- 表单元素的输入操作设置为防抖函数
- 每次用户输入完毕后将输入的结果再表单元素的下方展示
运行效果:
部分代码:
<style>
input {
display: block;
width: 600px;
height: 50px;
padding: 0 20px;
font-size: 20px;
margin: 100px auto;
}
.message {
width: 600px;
margin: 0px auto;
font-size: 30px;
background-color: #ccc;
padding: 10px 20px;
color: red;
height: 30px;
}
</style>
<input type="text" placeholder="请输入.." class="input">
思考:是否可以拥有返回值
问题:因为 getUserAction
函数可能是有返回值的,所以我们也要返回函数的执行结果,但是因为使用了 setTimeout
延迟执行,导致func始终是异步执行的, 通过 func.bind(context, args)
调用后的返回值赋给变量,最后再 return
的时候,值将会一直是 undefined
,无法实现防抖函数返回返回值功能
解决方案:使用同步形式给防抖函数提供返回值功能,即防抖函数频繁调用时立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。并通过添加一个 immediate
参数指定是否开启此功能。
// immediate 布尔值是否开启立即触发形式的防抖功能从而实现防抖函数有返回值效果
function debounce(func, wait, immediate) {
// result 变量保存立即执行后函数的返回值
var timeout, result;
return function () {
var context = this;
var args = arguments;
// 优化判断避免第一次调用函数清除上一次不存在的计时器id(虽然不会报错)
if (timeout) {
clearTimeout(timeout);
}
// 判断是否开启立即执行模式函数防抖
if (immediate) {
// 只有第一次执行时timeout不存在,callNow为真
var callNow = !timeout;
// 指定wait时间内,timeout都不可能为空,直到指定时间结束后timeout值为null,才可以重新触发执行
timeout = setTimeout(function(){
timeout = null;
}, wait)
// 如果已经执行过,不再执行
if (callNow) {
// 使用apply方法指定this并立即执行
result = func.apply(context, args)
}
}
else {
// 不使用立即执行模式,没有返回值
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
}
}
container.onmousemove = debounce(getUserAction, 1000, true);
查看效果:
思考:取消防抖函数
在真实的开发中,函数的防抖应用场景中经常需要取消防抖函数的功能,如:在微博中下拉加载功能(每次用户的下拉加载操作应用都要向服务器发送网络请求获取最新的微博信息)若下拉操作只有等 10 秒后才能重新触发事件,并且可能因为设备网络信号原因需要在10秒内取消本次加载操作的应用场景。
为了实现这一功能,我们需要知道在JavaScript中函数也是一个特殊的对象。所以将取消函数作为防抖函数的取消方法(属性)一并返回就可以实现取消防抖函数的效果
function debounce(func, wait, immediate) {
var timeout, result;
// 使用变量debounced接受防抖函数
var debounced = function () {
var context = this;
var args = arguments;
if (timeout) {
clearTimeout(timeout);
}
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
// 如果已经执行过,不再执行
if (callNow) {
result = func.apply(context, args)
}
}
else {
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
return result;
};
// 通过变量debounced 给防抖函数添加一个属性方法cancel,该方法内部用来清除延迟函数并且将timeout设置为null
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
// 将防抖函数返回
return debounced;
}
// 测试取消防抖函数执行
var count = 1;
var container = document.getElementById('container');
function getUserAction(e) {
container.innerHTML = count++;
};
// 获取设置了防抖的函数
var setUseAction = debounce(getUserAction, 10000, true);
// 将防抖函数绑定给频繁触发的事件
container.onmousemove = setUseAction;
// 将取消指定防抖函数的方法绑定给其他事件
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
})
效果如图:
以上就是函数防抖的所有概念了。
函数的节流
介绍
节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。
函数节流的实现
概念:根据上面的介绍我们可以理解函数的节流就是如果用户持续触发事件,每隔一段时间,只执行一次事件。
在函数的届六中中首次触发是否执行以及结束后是否执行,不同需求,实现的方式也有所不同。目前实现函数节流有两种主流方式:
- 使用时间戳
- 设置定时器。
我们用 leading
代表首次是否执行,trailing
代表结束后是否再执行一次。
使用时间戳
原理:当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,并更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = new Date().getTime();
// var now = +new Date(); 也可转化为时间戳
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
container.onmousemove = throttle(getUserAction, 1000);
效果演示如下:
我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。
使用定时器
原理:当触发事件的时候,设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行完毕,然后执行函数,清空定时器,再设置下个定时器。
function throttle(func, wait) {
var timeout;
var previous = 0;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}
效果演示如下:
我们可以看到:当鼠标移入的时候,事件不会立刻执行, 3s 后执行了一次,此后每 3s 执行一次,当数字显示为 3 后,移出鼠标停止触发,但是依然会在第 12s 的时候执行一次事件。
比较两个方法:
- 使用时间戳事件会立刻执行,使用定时器事件会在 n 秒后第一次执行
- 使用时间戳事件停止触发后没有办法再执行事件,使用定时器事件停止触发后依然会再执行一次事件
优化
需求:开发中我们希望能指定是否立刻执行,或停止触发的时候是否能再执行一次。
这时那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
leading:false
表示禁用第一次执行trailing: false
表示禁用停止触发的回调
代码实现:
function throttle(func, wait, options) {
// 计时器id, this, 参数
var timeout, context, args
// 上一次的时间
var previous = 0;
// 判断是否设置配置选项options
if (!options) {
options = {};
}
// 创建节流函数
var throttled = function() {
// 获取时间戳
var now = new Date().getTime();
// previous 为 0 即第一次调用,且 禁用第一次执行时
if (!previous && options.leading === false) {
previous = now;
}
// 事件触发间隔与 规定间隔时间(wait) 差值
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了
// 若禁用第一次执行,第一次执行时 remaining = wait 不会进入该判断
if (remaining <= 0) {
// 清除上一次计时器
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
// 更新本次时间
previous = now;
// 立即调用函数
func.apply(context, args);
// 清理工作 js 的垃圾回收
if (!timeout) {
context = args = null;
}
// 还有剩余时间,但是想添加一个最后指定时间的触发回调
} else if (!timeout && options.trailing !== false) {
// 这里是函数防抖
// !timeout 还未添加回调,已经添加结束回调不执行次代码
// ptions.trailing !== false 不禁用停止触发的回调
// 创建最后一次触发回调
timeout = setTimeout(later, remaining);
// 清理工作 js 的垃圾回收
if (!timeout) {
context = args = null;
}
}
};
var later = function() {
// 事件结束后根据是否立即执行重新设置previous
// 无需立即执行将previous设置成0
// 需要立即执行previous设置成本次方法触发的时间
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
func.apply(context, args);
};
return throttled;
}
注意:上面的代码的实现中有这样一个问题:就是 leading:false 和 trailing: false 不能同时设置。(在真实开发中不可能遇到leading:false 和 trailing: false都需要为false的情况)
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
取消
在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
ug 就出来了,所以,这个 throttle 只有三种用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
取消
在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
以上就是全部函数节流的概念