前言
在前端开发中会遇到一些频繁的事件触发,比如: window 的 resize、scroll;mousedown、mousemove;keyup、keydown
举个例子
debounce.html
<!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 src="debounce.js"></script>
</body>
</html>
debounce.js
var count = 1;
var container = document.getElementById('container');
function getUserAction(){
container.innerHTML = count++;
}
container.onmousemove = debounse(getUserAction, 1000, true);
为了解决这个问题,一般有两种解决方案: 1、防抖debounce;2、节流throttle
1、防抖
防抖debounce原理:如果用手指一直按住一个弹簧,它将不会弹起直到你松手为止。你尽管触发事件,但是我一定在事件触发n秒后才执行,如果你在一个事件触发的n秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件n秒内不再触发事件,我才执行,真是任性呐!
第一版
function debounce(fn, wait){
let timer = null;
return function(){
clearTimeout(timer);
timer = setTimeout(fn, wait);
}
}
//在上面例子基础上使用
container.onmousemove = debounce(getUserAction, 1000);
现在随你怎么移动,反正你移动完1000ms内不再触发,我才执行事件。看看使用效果。
第二版,解决this问题
debounce返回函数中this,是container元素,但是getUserAction执行中this确是window。解决办法
function debounce(fn, wait) {
let timer = null;
return function () {
var context = this;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context);
}, wait);
}
}
第三版,解决event问题
js 在事件处理函数中会提供事件对象 event。
function getUserAction(event){
console.log(event);
container.innerHTML = count++;
}
function debounce(fn, wait) {
let timer = null;
return function () {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, wait);
}
}
第四版,添加立即执行参数
不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(fn, wait, immediate) {
let timer = null;
return function () {
var context = this;
if (timer) clearTimeout(timer);
if (immediate){
var callNow = !timer;
timer = setTimeout(function() {
timer = null;
}, wait);
if (callNow) fn.apply(context, arguments);
} else {
timer = setTimeout(function () {
fn.apply(context, arguments);
}, wait);
}
}
}
第五版,有返回值
此时注意一点,就是getUserAction函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将func.apply(context,args)的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate为true 的时候返回函数的执行结果。
function debounce(fn, wait, immediate) {
let timer = null, result;
return function () {
var context = this;
if (timer) clearTimeout(timer);
if (immediate){
var callNow = !timer;
timer = setTimeout(function() {
timer = null;
}, wait);
if (callNow) result = fn.apply(context, arguments);
} else {
timer = setTimeout(function () {
fn.apply(context, arguments);
}, wait);
}
return result;
}
}
第六版,有取消事件
希望能取消 debounce函数,比如说我debounce的时间间隔是1秒钟,immediate 为true,这样的话,我只有等1秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行了。
function debounce(fn, wait, immediate) {
let timer = null, result;
var debounced = function () {
var context = this;
if (timer) clearTimeout(timer);
if (immediate){
var callNow = !timer;
timer = setTimeout(function() {
timer = null;
}, wait);
if (callNow) result = fn.apply(context, arguments);
} else {
timer = setTimeout(function () {
fn.apply(context, arguments);
}, wait);
}
return result;
}
debounced.cancel = function () {
clearTimeout(timer);
timer = null;
};
return debounced;
}
使用
var count = 1;
var container = document.getElementById('container');
var button = document.getElementById('button');
var setUserAction = debounce(getUserAction, 1000, true);
function getUserAction(e){
container.innerHTML = count++;
}
button.addEventListener('click', function () {
setUserAction.cancel();
});
container.onmousemove = setUserAction;
防抖的适用场景
按钮提交场景,多次点击提交按钮,只执行最后一次提交。
服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
节流
函数节流throttle:如果你持续触发事件,每隔一段时间,只执行一次事件。
用时间戳
function throttle(fn, wait){
var prevTime = 0;
return function () {
var nowTime = +new Date();
var context = this;
var args = arguments;
if (nowTime - prevTime > wait){
fn.apply(context, args);
prevTime = nowTime;
}
}
}
开始立即执行,鼠标移除立即停止
用定时器
function throttle2(fn, wait) {
var timer = null;
return function () {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, wait);
}
}
}
container.onmousemove = throttle2(getUserAction, 3000);
开始不会立即执行,鼠标移除出后还会在n秒后执行一次
二者结合
开始立即执行,最后移出后n秒还执行一次
function throttle3(fn, wait) {
var timer = null;
var preTime = 0;
return function () {
var context = this;
var nowTime = +new Date();
var remaining = wait - (nowTime - preTime);
if (remaining <= 0 || remaining > wait){
if (timer){
clearTimeout(timer);
timer = null;
}
preTime = nowTime;
fn.apply(context, arguments);
} else if (!timer) {
timer = setTimeout(() => {
timer = null;
preTime = +new Date();
fn.apply(context, arguments);
}, wait);
}
}
}
复合场景
但是我有时也希望无头有尾,或者有头无尾,这个咋办?
那我们设置个options作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
- leading:false 表示禁用第一次执行
- trailing: false 表示禁用停止触发的回调
function throttle3(fn, wait, options) {
var timer = null;
var preTime = 0;
return function () {
var context = this;
var nowTime = +new Date();
if (!preTime && options.leadig === false) {
//开始不立即执行
preTime = nowTime;
}
var remaining = wait - (nowTime - preTime);
//间隔时间等于或超过wait
if (remaining <= 0 || remaining > wait){
if (timer){
clearTimeout(timer);
timer = null;
}
preTime = nowTime;
fn.apply(context, arguments);
} else if (!timer && options.trailing !== false) {
//有一个定时器就不在执行,结束再执行一次
timer = setTimeout(() => {
timer = null;
preTime = options.leading === false ? 0 : +new Date();
fn.apply(context, arguments);
}, wait);
}
}
}
添加取消
function throttle3(fn, wait, options) {
var timer = null;
var preTime = 0;
var throttled = function () {
var context = this;
var nowTime = +new Date();
if (!preTime && options.leadig === false) {//开始不立即执行
preTime = nowTime;
}
var remaining = wait - (nowTime - preTime);
//间隔时间等于或超过wait
if (remaining <= 0 || remaining > wait){
if (timer){
clearTimeout(timer);
timer = null;
}
preTime = nowTime;
fn.apply(context, arguments);
} else if (!timer && options.trailing !== false) {//有一个定时器就不在执行,结束再执行
timer = setTimeout(() => {
timer = null;
preTime = options.leading === false ? 0 : +new Date();
fn.apply(context, arguments);
}, wait);
}
}
throttled.cancel = function () {
clearTimeout(timer);
timer = null;
preTime = 0;
}
return throttled;
}
我们要注意 underscore 的实现中有这样一个问题:
那就是 leading:false 和 trailing: false 不能同时设置。
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:
container.onmousemove = throttle3(getUserAction, 3000);
container.onmousemove = throttle2(getUserAction, 3000, {
leading: false
});
container.onmousemove = throttle2(getUserAction, 3000, {
trailing: false
});
节流适用场景
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
缩放场景:监控浏览器resize
动画场景:避免短时间内多次触发动画引起性能问题