哈喽,我是前端菜鸟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) }
}
注意点
- 防抖的实现其实利用了许多javascript基础,如闭包建立定时器们的联系,apply改变this取值,ES6箭头函数的作用域等
- 利用定时器setTimeout实现防抖,根据setTimeout返回id清除定时器达到防重复触发效果,有点类似websocket的心跳测试
- setTimeout有个问题是它不是很精确,例如设置了10ms,但在9ms后有个同步任务,或者微任务在占用执行栈执行,就会导致setTimeout延后执行,因为setTimeout是宏任务,具体可以参考一下这篇事件循环。
setInterval弊端和解决方案
既然谈到了setTimeout的弊端,那顺便说说它老兄弟setInterval的吧
setInterval存在以下弊端:
- 不关心报错,即使调用代码错了也会一直不断执行
(实锤愣头青 = =) - 无视网络延迟,使用ajax出现网络延迟时候,没等数据返回,照样一次一次请求接口
(愣头青X2) - 时间可能并不准确,和setTimeout一样,属于宏任务,当有其他任务优先级比它高在它之前执行,就会导致上一次的延后,那下一次调用时候,发现上一次还存在,就会跳过这一次,就会导致缺失次数或者时间不定
解决方案:
公认的是通过setTimeout代替setInterval
setTimeout(function () {
// 处理代码
setTimeout(arguments.callee, interval)
}, interval)
分析:
- 每次函数执行都会创建一个新的定时器
- 第二个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)
}
}
分析:
-
第一次进来timer为null,callNow为true,立即执行,这时候生成了个定时器
-
第二次假如在规定时间内,定时器还没生效,timer存在,callNow为false,清空定时器重新生成
-
规定时间外,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)
}
}
}
分析:
- 第一次进来,生成定时器
- 第二次在规定时间进来,有定时器了返回
- 第三次规定时间外,清除上一个定时器,定义下一个
应用场景
- 底部触发
- 搜索时候节流
结语
希望能给你带来帮助✨~
分享不易,点赞鼓励🤞