前言
防抖与节流是前端领域不可或缺的知识点。刚开始学习的时候容易搞混这两个概念。我们先来自己实现一下,然后再分析lodash的实现方式。
防抖
事件触发后的一段时间内没有再次触发事件,绑定的函数才会执行。
常见的场景有
- 用户输入内容,实时搜索
- 键盘的按下抬起事件
- 鼠标移动事件
- 滚动条移动事件
这些事件的触发比较密集,很容易造成浏览器卡顿。
用图来表示一下
上图中
- A事件延迟期间,B事件触发,则A事件在延迟结束后不会执行函数。
- B事件在延迟结束后会正常执行函数
- C事件在延迟结束后会正常执行函数
简易版实现
我们假设每次事件的触发都是有效的(延迟结束后执行函数),后续的事件触发会把上一次的事件置为无效(取消定时器)。
//简易版实现
function myDebounce1(callback,duration){
let timerId
return function(){
if(timerId!== undefined){
clearTimeout(timerId)
}
timerId = setTimeout(function(){
callback()
},duration)
}
}
简易版实现是远远不够的,作为调用方来讲,调用防抖函数应该跟直接绑定执行函数一样方便。我们进一步优化一下。
更改this指向
原执行函数内部的this
指向的应该是触发事件的元素
//更改this指向
function myDebounce2(callback,duration){
let timerId,context
return function(){
//这里的this指向的是事件触发元素
context = this
if(timerId!== undefined){
clearTimeout(timerId)
}
timerId = setTimeout(function(){
//apply更改函数的this指向
callback.apply(context)
},duration)
}
}
传入参数
事件执行函数的第一个参数应该是event
对象,下面的两种方式效果是一样的。
//传入参数
function myDebounce3(callback,duration){
let timerId,context,arg
return function(){
context = this
//arguments 里包含了传入的参数
arg = arguments
if(timerId!== undefined){
clearTimeout(timerId)
}
timerId = setTimeout(function(){
//apply指定参数
callback.apply(context,arg)
},duration)
}
}
另一种写法
//传入参数 另一种写法
function myDebounce4(callback,duration){
let timerId,context
return function(event){
context = this
if(timerId!== undefined){
clearTimeout(timerId)
}
timerId = setTimeout(function(){
//apply指定参数
callback.call(context,event)
},duration)
}
}
立即执行
有时候我们希望第一次事件触发要立即执行
还是借用这张图
立即执行的情况下变成
- A事件触发时执行函数
- B事件触发时,A事件的延迟事件还未结束,所以B事件不执行函数
- C事件触发时执行函数
可以看到,立即执行情况下,上次的事件会阻止后续的事件执行函数,和延迟执行正好相反。事件延迟期间,新的事件会延长等待时间。
//立即执行
function myDebounce5(callback,duration,immdiate=false) {
let timerId,context,flag
//是否立即执行
return function(event) {
context=this
if(timerId!==undefined) {
clearTimeout(timerId)
}
if(immdiate) {
//事件触发的同时,持有timerId直到延迟结束
if(timerId===undefined) {
callback.call(context,event)
}
//上一次事件延迟结束,释放timerId,后续的事件才能触发函数
timerId=setTimeout(function() {
clearTimeout(timerId)
timerId=undefined
},duration)
} else {
timerId=setTimeout(function() {
callback.call(context,event)
},duration)
}
}
}
取消
如果我们想随时取消防抖,就需要对外暴露方法。
对于立即执行方式来说,取消可以使下次事件必定执行函数
对于延迟执行方式来所,取消可以让有效事件变成无效事件
//增加取消机制
function myDebounce6(callback,duration,immdiate=false) {
let timerId,context
//是否立即执行
return function(event) {
context=this
if(timerId!==undefined) {
clearTimeout(timerId)
}
if(immdiate) {
//事件触发的同时,持有timerId直到延迟结束
if(timerId===undefined) {
callback.call(context,event)
}
//上一次事件延迟结束,释放timerId,后续的事件才能触发函数
timerId=setTimeout(function() {
clearTimeout(timerId)
timerId=undefined
},duration)
} else {
timerId=setTimeout(function() {
callback.call(context,event)
},duration)
}
}
//取消防抖
debounced.cancel=function() {
if(timerId!==undefined) {
clearTimeout(timerId)
}
context=timerId=undefined
}
return debounced
}
节流
每隔一段时间,才会执行一次函数。把高频无效的触发转为有效的低频触发。
同样用图说明
节流的实现方式,有时间戳方式和计时器方式
时间戳方式
判断上次事件触发距离现在的时间间隔是否大于指定时间间隔。
首次触发会立即执行,事件延迟期间,不会有新的函数执行。
对于上图:
- A事件在触发时执行函数
- B事件不会执行函数
- C事件在触发时执行函数
//时间戳方式
function myThrottle1(callback,duration) {
let dateObj=new Date()
let begin,context,arg
return function() {
context = this
arg = arguments
let dateObj=new Date()
let current=dateObj.getTime()
if(begin === undefined || current-begin>=duration) {
callback.apply(context,arg)
begin=current
}
}
}
计时器方式
首次触发会创建计时器,首次并不会立即执行。延迟时间结束后会执行函数。在延迟期间不会创建新的计时器。
对于上图:
- A事件在延迟结束时触发函数
- B事件不会创建计时器,也不会执行函数
- C事件在延迟结束时触发函数
//计时器方式
function myThrottle2(callback,duration){
let timerId,context,arg
return function(){
context = this
arg = arguments
if(timerId === undefined){
timerId = setTimeout(function(){
callback.apply(context,arg)
timerId = undefined
clearTimeout(timerId)
},duration)
}
}
}
优化
上面两种方式的问题在于。B事件不会执行函数。我们可以做到,A事件立即执行,B事件在A事件延迟结束后立即执行,同样C事件在B事件延迟结束后立即执行,这样更加平滑,用户不会感受到事件确实。