一篇读懂防抖与节流原理解析及应用,延伸至setTimeout、setInterval弊端和解决方案✨

575 阅读5分钟

哈喽,我是前端菜鸟JL😄 下面分享一下防抖节流这个专题

防抖

触发事件后n秒内只执行一次,如果重新触发,则时间重置

防抖又分立即执行版和非立即执行版

非立即执行版:

非立即执行版的意思是触发事件后不会立即执行,如果在规定n时间内再次触发,则时间重新计算。

// HTML版
<div onclick="bound(say, 1000)">
...
function say () { console.log('test debound') }
function bound(fn, delay) {
    let timer = null // 利用闭包原理建立每个独立定时器函数联系
    return function () {
        // setTimeout创建的定时器会返回一个ID值即timer
        // 利用这个ID值配合cleartimeout可以取消要延迟执行的代码块
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
         // 这里注意,箭头函数存在才直接用this,arguments
         // 由于setTimeout如果不是箭头函数,this会指向window,
         // 所以不是箭头函数的时候,需要用变量把this,arguments存起来再调用
         // 例如 context = this,arg = arguments
            fn.apply(this, arguments)        
        }, delay)    
    }
}

// vue版本
<button @click="say"></button>
...
methods: {
    test() {
        console.log(111)    
    },
    debound (fn, delay) {
        let timer = null
        return function () {
            if (timer) clearTimeout(timer)
            timer = setTimeout(() => {
                fn.apply(this, arguments)            
            }, delay)
        }    
    }
},
mounted() {
    this.test = this.bound(this.test, 1000)
}

// vue自定义指令版本(常用)
Vue自定义指令有全局注册和局部注册两种方式,这里介绍一下全局方式
主要通过Vue.directive(id, [definition])方式注册全局指令
然后在入口文件中进行Vue.use()调用

// 常见批量注册指令,新建directives/index.js文件
import debound from ./debound
// 自定义指令
const directives = {
    debound
}
export default {
    install(Vue) {
        Object.keys(directives).forEach((key) => {
            Vue.directive(key, directives[key])        
        })    
    }
}
// 在main.js引入并调用
import Vue from 'vue'
import Directives from './JS/directives'
Vue.use(Directives)

// 自定义防抖debounce指令
const debounce = {
    inserted: function (el, binding) {
        const delay = binding.value.dealy
        const fn = binding.value.fn
        let timer = null
        el.addEventListener('click', () => {
            if (timer) clearTimeout(timer)
            timer = setTimeout(() => {
                fn()                         
            }, 1000)        
        })
    }
}

// 应用
<button v-debounce="{fn: test, delay: 1000}"></button>
...
method: {
    test() { console.log(111) }
}

注意点

  1. 防抖的实现其实利用了许多javascript基础,如闭包建立定时器们的联系,apply改变this取值,ES6箭头函数的作用域等
  2. 利用定时器setTimeout实现防抖,根据setTimeout返回id清除定时器达到防重复触发效果,有点类似websocket的心跳测试
  3. setTimeout有个问题是它不是很精确,例如设置了10ms,但在9ms后有个同步任务,或者微任务在占用执行栈执行,就会导致setTimeout延后执行,因为setTimeout是宏任务,具体可以参考一下这篇事件循环

setInterval弊端和解决方案

既然谈到了setTimeout的弊端,那顺便说说它老兄弟setInterval的吧

setInterval存在以下弊端:

  1. 不关心报错,即使调用代码错了也会一直不断执行(实锤愣头青 = =)
  2. 无视网络延迟,使用ajax出现网络延迟时候,没等数据返回,照样一次一次请求接口(愣头青X2)
  3. 时间可能并不准确,和setTimeout一样,属于宏任务,当有其他任务优先级比它高在它之前执行,就会导致上一次的延后,那下一次调用时候,发现上一次还存在,就会跳过这一次,就会导致缺失次数或者时间不定

解决方案:

公认的是通过setTimeout代替setInterval

setTimeout(function () {
    // 处理代码
    setTimeout(arguments.callee, interval)
}, interval)

分析:

  1. 每次函数执行都会创建一个新的定时器
  2. 第二个setTimeout调用使用了arguments.callee来获取对当前执行函数的引用,并为其设置了另一个定时器,确保前一个执行完之前不会向队列插入新的定时器代码,以及丢失间隔和次数,至少需要等待指定间隔。

setTimeout最低时延4ms

不同浏览器最低时延不一样,例如chrome最低时延是1ms,但假如timer嵌套层次很多,最低时延就会变成4ms

嵌套层次也是不同浏览器有所不同,但是一般以5为界限

function debound(fn, delay) {
    let timer = null
    return function () {
        if (timer) clearTimeout(timer)
        let callNow = !timer
        timer = setTimeout(() => {
            timer = null        
        }, delay)    
        if (callNow) fn.apply(this, arguments)
    }
}

分析:

  1. 第一次进来timer为null,callNow为true,立即执行,这时候生成了个定时器

  2. 第二次假如在规定时间内,定时器还没生效,timer存在,callNow为false,清空定时器重新生成

  3. 规定时间外,timer=null,走回第一步

合并版本(防抖——立即+不立即执行)

立即执行版本(防抖)

/**
 * @description 函数防抖
 * @param {*} fn 函数
 * @param {Number} delay 延迟时间,单位毫秒
 * @param {Boolean} immediate true 立即执行,false 非立即执行
 */
 function debounce(fn, delay, immediate) {
     let timer = null
     return function () {
         if (timer) clearTimeout(tiimer)
         if (immediate) {
             let context = this
             let callNow = !timer
             timer = setTimeout(() => {
                 timer = null             
             }, delay)
             if (callNow) fn.apply(context, arguments)         
         } else {
             timer = setTimeout(() => {
                 fn.apply(context, arguments)             
             }, delay)         
         }            
     } 
 }

应用场景

  • 滚动事件
  • 文本框输入请求接口(主要场景)
  • 鼠标点击按钮等 回到正题,立即执行版本即触发立即执行,然后在规定时间触发则重置时间

节流

规定时间内只能触发一次,如果在规定时间触发多次事件,只能执行一次,时间不重置

节流会稀释执行频率

对于节流有两种实现方式:时间戳版和定时器版

时间戳版

    let pre = 0
    return function () {
        let now = new Date()
        if (now - pre > delay) {
            previous = now
            fn.apply(this, arguments)        
        }    
    }
}

定时器版

    let timer = null
    return function () {
        const context = null
        if (!timer) {
            timer = setTimeout(() => {
                timer = null
                fn.apply(context, arguments)            
            }, delay)        
        }    
    }
}

分析:

  1. 第一次进来,生成定时器
  2. 第二次在规定时间进来,有定时器了返回
  3. 第三次规定时间外,清除上一个定时器,定义下一个

应用场景

  • 底部触发
  • 搜索时候节流

结语

希望能给你带来帮助✨~

分享不易,点赞鼓励🤞