【JS】防抖和节流

130 阅读4分钟

一、防抖

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 <= 0timer 为 undefined,使 preTime = nowTime,执行一次回调

② 第二次触发:分两种情况

a. delay 时间未到:remainTime > 0,且 timer 仍为 undefined,则进入 else if(!timer) 的判断,设置一个定时器,remainTime 时间后再执行定时器回调(更新之前时间为当前时间,置 timer 为 null,执行一次回调)

b. delay 时间已到:remainTime <= 0,和第一次触发一样的逻辑

③ 以后再次触发时,timer 为 undefined 或 null,取决于是否执行了至少一次 timer = null,但无论为何值,执行逻辑不变

3. 应用

(1) 鼠标不断的点击,用节流来限制只在规定的单位时间内执行一次函数

(2) 滚轮事件, 不断的往下滚轮,比如滚动到底部加载数据