一、防抖
1. 概念
每触发一次,重新计时;若触发后在计时时间内没有再次触发,则执行回调
2. 实现
(1) 非立即执行
function debounce(fn, delay){
let timer = null
return function(){
if(timer) clearTimeout(timer) // 清空定时器,并不是将 timer 设为 null
timer = setTimeout(() => { // 一定要写箭头函数,不然定时器的 this 默认指向 window
console.log(this, arguments)
fn.apply(this, arguments) // 让 this 指向事件源对象
}, delay)
}
}
(2) 立即执行
function debounce(fn, delay){
let timer = null
return function(){
if(timer) clearTimeout(timer) // 清空定时器,并不是将 timer 设为 null
let runNow = ! timer
timer = setTimeout(() => {
timer = null
}, delay)
if(runNow) {
console.log(this, arguments)
fn.apply(this, arguments)
}
}
}
执行逻辑
① 第一次触发:timer 为 null,runNow 为 true,设置 timer 指向定时器,执行一次回调
② 第二次触发:分两种情况
a. delay 时间没到:timer 不为 null,清除上一个定时器(注意:并不是把 timer 设为了 null),runNow 为 false,重新设置 timer 指向新的定时器(即重新计时),因为 runNow 为 false,故回调不执行
b. delay 时间已到:timer 设为了 null,上一个定时器没有被指向,所以被自动回收,runNow 此时为 true,设置 timer 指向新的定时器(即再次计时),因为 runNow 为 true,故执行一次回调
(3) 完整版
<body>
<input type="text" id="input">
</body>
<script>
let input = document.getElementById("input")
function changeInput(){
console.log("输入改变了")
}
input.addEventListener("input", debounce(changeInput, 1000, true))
function debounce(fn, delay, immediate = false){
let timer = null
return function(){
if(timer) clearTimeout(timer)
if(immediate){ // 立即执行
let runNow = ! timer
timer = setTimeout(() => {
timer = null
}, delay)
if(runNow) fn.apply(this, arguments)
} else { // 非立即执行
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
}
</script>
3. 应用
(1) 搜索输入框搜索内容,用户在不断的输入的时候,用防抖来节约请求资源
(2) 不断的触发 window 的 resize 事件,不断的改变窗口大小,利用防抖函数来只执行一次
二、节流
1. 概念
当持续触发事件时,保证一定时间段内只执行一次回调
2. 实现
(1) 时间戳
function throttle(fn, delay){
let preTime = 0
return function(){
let nowTime = +new Date()
// 如果 (当前时间 - 之前时间)已经大于 设置的时间
if(nowTime - preTime > delay){
// 执行一次回调
fn.apply(this, arguments)
// 重新设置 之前时间 为 当前时间
preTime = nowTime
}
}
}
特点:
a. 第一次触发,回调立即执行
b. 只要停止触发,回调就不会执行
(2) 定时器
function throttle(fn, delay){
let timer = null
return function(){
// 如果定时器为 null,说明单位时间已到,则重新触发后,设置定时器,等待执行回调
if(!timer){
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
}
特点:
a. 第一次触发,回调会延迟 delay 再执行
b. 停止触发后,会再次执行一次回调
(3) 两者结合
目的:将两者结合起来是要实现一个既能开始时执行一次回调,又能结束时再执行一次回调
function throttle(fn, delay){
// 定时器变量,上下文对象,参数列表,之前触发时间
let timer, context, args, preTime=0
return function(){
// 获取当前时间
let nowTime = +new Date()
// 计算距离下次执行的时间
let remainTime = delay - (nowTime - preTime)
// 保存上下文对象和参数列表
context = this
args = arguments
// 如果距离下次执行的时间小于 0 (说明时间已到)或者 修改了系统时间
if(remainTime <= 0 || remainTime > delay){
if(timer){
// 清除上一次的定时器
clearTimeout(timer)
// 定时器变量指向空
timer = null
}
// 设置之前时间为当前时间
preTime = nowTime
// 执行一次回调
fn.apply(context, args)
} else if (!timer){ // 如果距离下次执行的时间大于 0(说明还未到下次执行的时间,需要设置个定时器再等一段时间(remainTime))
timer = setTimeout(() => {
// 定时器的回调执行
// 重新设置 之前时间 为 当前时间
preTime = +new Date()
// 将 定时器变量 置为 null
timer = null
// 执行一次回调
fn.apply(context, args)
}, remainTime)
}
}
}
执行逻辑
① 第一次触发:preTime = 0,所以 remainTime <= 0,timer 为 undefined,使 preTime = nowTime,执行一次回调
② 第二次触发:分两种情况
a. delay 时间未到:remainTime > 0,且 timer 仍为 undefined,则进入 else if(!timer) 的判断,设置一个定时器,remainTime 时间后再执行定时器回调(更新之前时间为当前时间,置 timer 为 null,执行一次回调)
b. delay 时间已到:remainTime <= 0,和第一次触发一样的逻辑
③ 以后再次触发时,timer 为 undefined 或 null,取决于是否执行了至少一次 timer = null,但无论为何值,执行逻辑不变