持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
一 防抖
触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。
<!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>
<input type="text" id="inp">
</body>
</html>
第一版 this & arguments
问题一: 使用debounce之后, getUserAction函数内的this指向window,不再指向container
问题二: event 无法传递到 getUserAction
'use strict'
let count = 1;
let container = document.getElementById('container');
function getUserAction(e) {
console.log(e);
// console.log(this);
// 作为setTimeout的回调函数,它的this指向window
container.innerHTML = count++;
};
function debounce(func, wait) {
//写一个计时器,到点才会进行下一次
let timeout;
return function () {
// console.log(this);
// 这里的this指的是container
clearTimeout(timeout)
// 解决问题二: 传递event对象
// console.log(arguments);
// 除了箭头函数,普通函数内部都有arguments
// 解决问题一: this的指向
// 使用apply 和 call 的话,需要用匿名函数
// () => func.apply(this,arguments)
// () => func.call(this,...arguments)
// bind返回的是一个函数, 可以直接使用
timeout = setTimeout(func.bind(this, ...arguments), wait);
}
}
container.addEventListener('mousemove', debounce(getUserAction, 1000));
第二版 立即执行
实现效果:第一次mousemove触发后,立即执行,等n 秒之后再触发才能再执行。
// timeout: setTimeOut函数会返回一个 timerId,我们用变量timeout储存这个id
// 向 clearTimeout 传入这个timerId 来删除这个计时器。
function debounce(func, wait, immediate) {
let timeout;
return function () {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
let callNow = !timeout;
timeout = setTimeout(function(){
//1st mousemove: timeout===undefined,于是执行第一次count++
//设置异步函数,到时间后timeout为null,--> timer1
//2~n mousemove: n秒内再次触发,
//timeout===timer1的id, 类真值 --> 清除timer1,
//timeout===timer1的id,callNow = !timeout = false,
//call setTimeout,设置timer2... timeout = null,等待下一个wait之后
//才能再次立即执行
timeout = null;
}, wait);
if (callNow) func.apply(this, arguments);
} else {
// immediate === false 执行第一版的函数
timeout = setTimeout(func.bind(this, ...arguments), wait);
}
}
}
第三版 返回结果
有些函数是会返回值的,这个版本是为了满足这个需求。
function debounce(func, wait, immediate) {
let timeout, result;
return function () {
if (timeout) clearTimeout(timeout);
if (immediate) {
// 如果已经执行过,不再执行
let callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(this, arguments);
}
else {
timeout = setTimeout(func.bind(this, ...arguments), wait);
}
// 立即执行才会有 getUserAction 返回的结果
// 否则执行异步函数,在getUserAction返回结果前,debounce就已经return了,此时返回的是undefined
return result;
}
}
第四版 取消
实现效果: 一键取消防抖
function debounce(func, wait, immediate) {
let timeout, result;
let debounced = function () {
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function(){
timeout = null;
}, wait)
if (callNow) result = func.apply(this, arguments);
}
else {
timeout = setTimeout(func.bind(this, ...arguments), wait);
}
return result;
};
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
}
使用方法:
const setUseAction = debounce(getUserAction, 10000, true);
container.addEventListener('mousemove', setUseAction);
document.getElementById("button").addEventListener('click', function(){
setUseAction.cancel();
// 这里要在function里面call,不然会在页面渲染的时候就执行
// 点击按钮不会再执行
});
二 节流
高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。每次触发事件时都判断当前是否有等待执行的延时函数。
第一版 使用时间戳timestamp
// 第一版
function throttle(func, wait) {
let previous = 0;
return function() {
let now = +new Date(); //把Now转化为数字保存
if (now - previous > wait) {
// 1st mousemove now - previous 一定大于 wait
func.apply(this, arguments); // 触发getuseraction
previous = now; // 用pre保存now 2~n次间隔都需要达到wait,才能触发getuseraction
}
}
}
container.addEventListener('mousemove', throttle(getUserAction, 3000));
如果是在wait期间停止触发,就不会再执行事件。
第二版 使用定时器
// 第二版
function throttle(func, wait) {
let timeout;
// 第一次mouseover后,不会有任何变化显示,等待wait事件结束才会触发getUserAction
return function() {
if (!timeout) { // 1st mousemove, timeout === undefined
// timeout = timer1的id
timeout = setTimeout(() => { // 这里用箭头函数, 会继承容器的this
timeout = null;
func.apply(this, arguments) // 在wait结束后触发getUserAction
// 如果在wait期间再次触发, 因为 timeout = timer1的id
// 所以会直接结束,没有任何操作
// 需要等到wait 结束timeout = null 之后,再次触发,进入下个循环
}, wait)
}
}
}
container.addEventListener('mousemove', throttle(getUserAction, 3000));
同样是在wait期间停止触发,就不会再执行事件。
对比:
第三版 两者结合
鼠标移入能立刻执行,停止触发的时候还能再执行一次。
// 第三版
function throttle(func, wait) {
let timeout;
let previous = 0;
let later = function(that) {
// 设置time = null, 可以进入下一个Block 2
previous = +new Date();
timeout = null;
func.apply(that, arguments)
};
let throttled = function() {
let now = +new Date();
// now - previous = interval
// remaining = wait - interval
//下次触发 func 剩余的时间
let remaining = wait - (now - previous);
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
// Block 1
// 1) wait - interval <= 0 -> interval >= wait
// 1.1 1st mousemove,remaing 必然 <= 0
// 1.2 later 执行后,过了超过wait时长,才再次触发
// 因为重新赋值了pre, 如果在wait之内,会进入Block 2
// 2) wait - interval > wait -> now - previous < 0 基本上不可能,除非是改了时间
// console.log(1);
if (timeout) { // 1.1的情况timeout = undefined不会进入
// console.log(3);
clearTimeout(timeout);
timeout = null;
// 这段if block 是给 2)设置的
// 已经设置了timer,被调了系统时间
// 把之前的timeout清除,timeout清空,立即之后,进入正常的循环
}
previous = now; // pre赋值时间戳
func.apply(this, arguments); // 1.1 1st mousemove 立即执行
} else if (!timeout) {
// Block 2
// 2nd mousemove 在wait期间内触发
// 等wait结束后触发later, 之后在此期间内如果多次触发
// 因为timeout有id, 不满足这两个条件,会直接结束
// console.log(2);
timeout = setTimeout(later.bind(null,this), remaining);
}
};
return throttled;
}
第四版 优化
有头无尾, 或者无头有尾。
没有无头无尾 {leading: false, trailing: false}
function throttle(func, wait, options) {
let timeout;
let previous = 0;
if (!options) options = {};
let later = function(that) {
previous = options.leading === false ? 0 : new Date().getTime();
// 1) leading = false pre = 0; 保证wait结束后再次触发还是进入Block 2
timeout = null;
func.apply(that, arguments);
};
let throttled = function() {
let now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
// 1) leading = false, 1st mousemove
// pre = 0 and leading = false
// remaining = wait - 0 > 0 进入Block 2
let remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
// Block 1
// 2) tailing = false
// 1st mousemove remaining注定<0
// 2~n wait 时间内, 会因为remaining > 0 且 trail = false直接走完函数
// 超过wait时间,满足remaining <= 0,再次进入block 1
// 3) lead = false && tail = false
// 因为trail = false,所以不能进入Block 2
// 1st 因为pre = 0, 所以 pre = now, 不进入任何block
// 2nd 在wait时间内, remaing > 0, 不进入任何block
// 超过wait时间,remaing < 0, 立即执行;
// 只有这一种方式能使count++,但是没做到无头无尾
// console.log(1);
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(this, arguments);
} else if (!timeout && options.trailing !== false) {
// Block 2
// 立即执行后,timeout = null, 且 remaining > 0
// 需要添加 trail != false 阻止进入Block 2
// console.log(2);
timeout = setTimeout(later.bind(null,this), remaining);
}
};
return throttled;
}
第五版 取消
...
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
// 添加到return前面,用法跟防抖第四版一样
...