在实际的开发场景中,一些高频次操作是不可避免的,如鼠标移动,滚动屏幕,键盘输入等,事件的高频率触发必然造成极大的性能开销,防抖 和 节流 就是针对此类场景进行优化,降低回调函数触发的频率,达到提高性能的目的
防抖
什么是防抖呢?简单来说就是在一定的时间内,无论事件触发多少次,都不执行事件的回调函数,而是等到最后一次事件触发的 t 秒后再执行事件回调,若在计时尚未结束时又触发了该事件,则重置 t ,重新开始计时
就像英雄联盟里的回城,当计时结束后才能成功回城,若中间被打断就要重新开始回城计时
下面来实现一个最基本的防抖函数:
function debounce(fn,t){//传入事件回调和等待时间
let timeId
return function () {
if(timeId){
clearTimeout(timeId) //当延时函数id存在时停止延时函数
}
timeId = setTimeout(function(){ //开启延时函数,重新计时
fn()
},t)
}
}
这样的实现虽然已经有了防抖的基本功能,但仍然存在一些问题,首先当回调函数需要传入参数时,上述实现显然不能处理,同时 setTimeout 函数的 this 在非严格模式下默认指向 Window ,当回调中需要使用 this 时显然不能正常执行,所以我们需要进行一些修改:
function debounce(fn,t){//传入事件回调和等待时间
let timeId
let context
let args
return function () {
context = this //记录this
args = arguments //接收回调的参数
if(timeId){
clearTimeout(timeId) //当延时函数id存在时停止延时函数
}
timeId = setTimeout(function(){ //开启延时函数,重新计时
fn.apply(context,args)//使用apply调用回调,改变其this指向,并传入参数
},t)
}
}
以上的防抖实现基本已经比较完善了,但有时候我们会面临这样一种场景,用户触发防抖事件后,我们想要首先给予用户一定的响应,再开启防抖,以优化用户体验,即在某些场景下,我们希望事件触发时,先执行一次回调,再开启防抖,接下来我们来实现这个功能:
function debounce(fn,t,immediate=false){//传入事件回调和等待时间,immeditae判断是否立即执行
let timeId
let execute
let res
let context
let args
return function () {
context = this //记录this
args = arguments //接收回调的参数
if(timeId){
clearTimeout(timeId) //当延时函数id存在时停止延时函数
}
if(immeditae){
execute = !timeId //当timeId不存在时,execute为true
timeId = setTimeout(function(){ //设置延时器,指定时间后设置为null
timeId = null
},t)
if(execute){
res = fn.apply(context,args)
}
}
else {
timeId = setTimeout(function(){ //开启延时函数,重新计时
fn.apply(context,args)//使用apply调用回调,改变其this指向,并传入参数
},t)
}
return res
}
}
当 immediate为 true 时,第一次执行时, timeId 为 undefined,此时 execute 为 true ,开启一个延时函数,在延时结束后清除 timeId, 并立即执行回调
第二次执行时,分为两种情况,一是设置的延时器已经结束,此时 timeId 已被置为 null, execute 为 true ,重新设置延时器,然后执行函数。另一种情况是,设置的延时器还没有结束,此时timeId不为 null ,execute 为 false , 重新设置延时器后,不会执行回调
接下来我们继续优化,在某些场景,我们想手动取消防抖的延时,并再次触发事件后立即执行回调,因此我们需要给防抖函数增加一个取消的功能:
function debounce(fn,t,immediate=false){//传入事件回调和等待时间,immeditae判断是否立即执行
let timeId
let execute
let res
let context
let args
cosnt debounced = function () {
context = this //记录this
args = arguments //接收回调的参数
if(timeId){
clearTimeout(timeId) //当延时函数id存在时停止延时函数
}
if(immeditae){
execute = !timeId //当timeId不存在时,execute为true
timeId = setTimeout(function(){ //设置延时器,指定时间后设置为null
timeId = null
},t)
if(execute){
res = fn.apply(context,args)
}
}
else {
timeId = setTimeout(function(){ //开启延时函数,重新计时
fn.apply(context,args)//使用apply调用回调,改变其this指向,并传入参数
},t)
}
return res
}
debounced.cancel = function() {//增加取消防抖功能,清除延时函数,并将timeId置空
clearTimeout(timeId);
timeId = null;
};
return debounced
}
以上就是 防抖 的完整实现了
节流
节流也是一种减少事件回调触发次数的手段,其思想是在规定的时间 t 内,无论事件触发多少次,其回调只执行一次,也就是说回调函数在 t 内只执行一次
如果说防抖是英雄联盟里的回城机制,那节流就是技能,只有当技能的冷却时间走完,处于冷却状态时才能使用技能,要是技能处于冷却状态,无论怎么点也使用不了
下面是节流函数的一个基本实现:
function throttle(fn, t) {
let timeout, context, args
// 起始时间
let startTime = 0
return function () {
// 得到当前的时间
let now = Date.now()
context = this
args = arguments
// 判断如果大于等于 t 则调用函数
if (now - startTime >= t) {
// 调用函数
fn.apply(context,args)
// 起始的时间 = 现在的时间
startTime = now
}
}
}
以上是通过 时间戳 来实现节流,也可以跟 防抖 的实现一样采用延时函数来实现:
function throttle(fn, t) {
let timeId, context, args;
return function() {
context = this
args = arguments
// 允许执行
if(!timeId) {
// 设置定时器,到达时间后设置timeId为null
timeId = setTimeout(function() {
timeId = null
fn.apply(context, args)
}, t)
}
}
在以上两种实现中,时间戳 的实现方式会在开始立即执行一次回调,且停止触发事件后不再执行回调,而 延时函数 的实现方式则是停止触发事件后依然会执行一次回调,且一开始会等待延时结束后再执行回调,我们可以将两者结合一下,实现一个既能开始时执行一次函数,又能结束时再执行一次函数的节流方法:
function throttle(fn, t) {
let timeId, context, args
let startTime = 0
const timeout = function() {//定义一个函数用于在不触发事件后执行回调
// 定时器执行时更新时间戳
startTime = Date.now()
timeId = null;
// 执行函数
fn.apply(context, args)
};
const throttled = function() {
//获取现在的时间戳
let now = Date.now()
//距下次调用剩下的时间
let restTime = t - (now - startTime)
context = this
args = arguments
if(restTime <= 0 || restTime > t){//若剩余时间小于等于0或者更改了系统时间
if (timeId) {//清空延时函数,并置id为空
clearTimeout(timeId);
timeId = null;
}
//更新时间戳并调用回调
startTime = Date.now()
fn.apply(context,args)
}
else if(!timeId){//若延时函数不存在则开启延时函数
//这里的延时应该设置为剩余时间,而不是t
timeId = setTimeout(timeout,restTime)
}
}
return throttled
}
当第一次调用时,startTime 为0, restTime 肯定小于0,所以执行回调,并更新时间戳为当前时间
第二次调用时,分为两种情况,第一种当计算 restTime 小于0时,即到达指定时间,执行函数,操作同第一次触发,第二种情况当指定时间未到达时,restTime大于0,会设置定时器,不会执行函数,同时在定时器中更新时间戳,即使不触发事件,当定时器回调执行时也会执行事件回调
上述的实现还可以继续优化,在某些场景需要自行控制节流函数是否在开始或停止触发事件后执行事件回调,可以设置第三个参数,通过 leading 和 trailing 属性分别控制开始和停止事件触发后回调的执行:
function throttle(fn, t, option = {}) {
let timeId, context, args
let startTime = 0
const timeout = function() {//定义一个函数用于在不触发事件后执行回调
// 定时器执行时更新时间戳,当leading为false时,将 startTime置0,保证开始触发事件时不执行回调
startTime = option.leading===falase ? 0 : Date.now()
timeId = null;
// 执行函数
fn.apply(context, args)
};
const throttled = function() {
//获取现在的时间戳
let now = Date.now()
//leading为false首次不执行,将startTime置为now,则restTime就是t,不进if
if(!startTime && option.leading===false) startTime = now
//距下次调用剩下的时间
let restTime = t - (now - startTime)
context = this
args = arguments
if(restTime <= 0 || restTime > t){//若剩余时间小于等于0或者更改了系统时间
if (timeId) {//清空延时函数,并置id为空
clearTimeout(timeId);
timeId = null;
}
//更新时间戳并调用回调
startTime = Date.now()
fn.apply(context,args)
}
else if(!timeId && option.trailing !== false){//若延时函数不存在且trailing不为false则开启延时函数
//这里的延时应该设置为剩余时间,而不是t
timeId = setTimeout(timeout,restTime)
}
}
return throttled
}
注意 leading 和 training 最好不要同时为 false 否则节流函数不能正常工作,默认情况下 option 为null,相当于 leading和training 都为 true
接下来跟 防抖 一样,再为 节流 添加上取消功能:
function throttle(fn, t, option = {}) {
let timeId, context, args
let startTime = 0
const timeout = function() {//定义一个函数用于在不触发事件后执行回调
// 定时器执行时更新时间戳,当leading为false时,将 startTime置0,保证开始触发事件时不执行回调
startTime = option.leading===falase ? 0 : Date.now()
timeId = null;
// 执行函数
fn.apply(context, args)
};
const throttled = function() {
//获取现在的时间戳
let now = Date.now()
//leading为false首次不执行,将startTime置为now,则restTime就是t,不进if
if(!startTime && option.leading===false) startTime = now
//距下次调用剩下的时间
let restTime = t - (now - startTime)
context = this
args = arguments
if(restTime <= 0 || restTime > t){//若剩余时间小于等于0或者更改了系统时间
if (timeId) {//清空延时函数,并置id为空
clearTimeout(timeId);
timeId = null;
}
//更新时间戳并调用回调
startTime = Date.now()
fn.apply(context,args)
}
else if(!timeId && option.trailing !== false){//若延时函数不存在且trailing不为false则开启延时函数
//这里的延时应该设置为剩余时间,而不是t
timeId = setTimeout(timeout,restTime)
}
}
throttled.cancel = function() {//取消节流
clearTimeout(timeId)
startTime = 0
timeId = null
}
return throttled
}
以上就是节流的完整实现
总结
以上实现的 防抖 和 节流 已经非常完善了,只是没有考虑到事件回调带有返回值的情况,这种情况出现得也比较少,要实现的话可以考虑两种思路,一是为防抖 和 节流 函数添加一个回调函数用来处理返回值,二是使用 Promise 获取返回值