函数节流和函数防抖算是性能优化的范畴,说起来是可用可不用的东西,但是,作为一个有追求的前端,当然是要灵活运用的~~。
一、函数节流
函数节流,就是一定周期内最多只执行一次。
形象点说,就像公交车行驶,以车子行驶作为触发事件,以乘客上车作为执行事件,公交会分成很多个站点,控制乘客只能在各个站点上车,不能中途上。
1、js封装
(函数节流的封装其实网上各种版本都有,很多版本是有问题的(例如没有立即执行,特殊场景就用不了),所以建议各位还是花时间去理解一下封装的原理和细节,这样才能知其然并知其所以然。)
/**
* 函数节流
* @param {function} fn 函数
* @param {number} t 间隔时间(毫秒)
* @return {function}
*/
function throttle (fn, t) {
let flag = true
let interval = t || 500
return function (...args) {
if (flag) {
fn.apply(this, args)
flag = false
setTimeout(() => {
flag = true
}, interval)
}
}
}
2、使用方式
无论是函数节流还是函数防抖,都是利用函数闭包共用同一函数作用域进行封装的,而函数是每执行一次就会生成一个新的函数作用域,所以使用时需格外注意,封装的节流或防抖方法是用来对我们真正要执行的函数做一层包装的,而且要只包装一次,保证包装后的函数能共用一个作用域。
例如:
import throttle from './throttle'
// 定义我们要执行的方法
const myFn = () => {
console.log('Neo')
}
// 对myFn做一次节流包装,生成了一个父级作用域以及使用了该作用域变量的函数throttledFn
const throttledFn = throttle(myFn, 1000)
// 应用节流,正确方式:使用throttledFn
setInterval(() => {
throttledFn()
}, 10)
// 应用节流,错误示例:
setInterval(() => {
throttle(myFn, 1000) // 这样使用每次都会生成一个独立的父级作用域,无法节流
}, 10)
vue中的使用技巧:
import throttle from './throttle'
export default {
computed: {
// 利用computed具有缓存的特性,对methods里的方法做节流包装,也能保证只会包装一次
throttledFn () {
return throttle(this.myFn, 1000)
}
},
methods: {
myFn () {
console.log('Neo')
}
},
mounted () {
// 使用示例
setInterval(() => {
this.throttledFn()
}, 10)
}
}
或者直接写在methods里:
methods: {
myFn () {
console.log('Neo')
},
// 正确示例,throttledFn正确指向节流包装一次后的函数引用地址()关键点:不要再使用function包装throttle
throttledFn: throttle(function () {
this.myFn()
}, 1000),
// 错误示例1,这里es6语法用函数包装了一层throttle,每次调用throttledFn都会重新包装一次,节流就失效了
throttledFn () {
return throttle(this.myFn, 1000)
},
// 错误示例2,this指向错误,这里的this指向了vue的methods对象之外,this值为undefined而非vue实例
throttledFn: throttle(this.myFn, 1000),
},
3、应用场景
(1) 页面滚动事件
- 例如判断页面滚动位置来控制返回顶部按钮的显示隐藏,就可以在滚动事件里做持续的节流处理。
(2) hybrid app 里h5跳转app原生页事件
- 这里的跳转事件如果不做节流处理,可能会出现快速双击多次会打开多个相同的原生页面,我的做法就是节流一秒,即一秒内只能触发一次跳转事件,一秒内页面差不多已经跳转过去了,基本能满足需求。
二、函数防抖
函数防抖,就是停下来一定时间后再执行。
形象点说,就像公交车行驶,以乘客上车作为触发事件,以车子行驶作为执行事件,司机在站点会停下来等乘客上车,在不再有乘客上车后才会发动车子行驶。
1、js封装
/**
* 函数防抖
* @param {function} fn 函数
* @param {number} t 等待时间(毫秒)
* @return {function}
*/
function debounce (fn, t) {
let timeId
let delay = t || 500
return function (...args) {
if (timeId) {
clearTimeout(timeId)
}
timeId = setTimeout(() => {
timeId = null
fn.apply(this, args)
}, delay)
}
}
2、使用方式
(略,同函数节流)
3、应用场景
(1) 典型的应用场景:搜索词联想
- 比如在搜索框里输入小米,会请求接口获取小米这个词的联想词,小米手机、小米笔记本、小米10什么什么的,如果不做防抖处理,直接监听input事件就请求接口,那这里就会同时请求了“小”和“小米”两个搜索词的接口,其实在你正在输入时是不需要实时请求接口的,这时候用防抖处理,在用户停止输入一秒后再请求接口,能大大减少不必要的请求,降低服务器压力。
三、综合运用
- 比如要做个鼠标移动盒子时让盒子跟随鼠标一起移动的需求,这个会有什么问题呢?
- 首先移动事件mousemove触发频率很高,需要做节流处理,但是如果鼠标瞬间移动,而且这个移动时间间隔小于节流时间间隔,就会出现最终盒子没在鼠标位置的bug,因为在终止移动的那一刻未达到节流周期所以没有触发执行函数。
- 这时候我们其实只是需要一个“兜底”,就是保证mousemove事件的最后一次触发时会执行我们的处理函数,怎么解决?
- 其实只需要节流+防抖一起用,并设置防抖的时间间隔大于节流的时间间隔,节流提升性能并保证能立即执行,防抖保证兜底,就ok了。
四、typescript版的封装
- ts封装的问题点就是this的处理,不做处理时this会被ts检测到报错信息:“this” 隐式具有类型 “any”,因为它没有类型注释。
- 解决方法就是在return的function函数参数里加一个this声明,(具体代码在下面),放心,this指向不会变,仍然是谁调用就指向谁。
- 严格的讲,这里声明的this并不算是一个参数,首先这个命名必须是this,然后必须放在function参数的第一项,这样才能保证this就是我们想要的函数调用者,如果换了个命名或者换了下参数顺序就变成一个普通参数了,这里不能用常规的方式去理解,只能猜想是ts针对this做了一个特殊处理。
- 关于ts里this的用法可以参考官方文档
- 上代码:
// 节流
function throttle (fn: { apply: (arg0: any, arg1: any[]) => void }, t: number) {
let flag = true
const interval = t || 500
return function (this: any, ...args: any) {
if (flag) {
fn.apply(this, args)
flag = false
setTimeout(() => {
flag = true
}, interval)
}
}
}
// 防抖
function debounce (fn: { apply: (arg0: any, arg1: any) => void }, t: number) {
let timeId: any = null
const delay = t || 500
return function (this: any, ...args: any) {
if (timeId) {
clearTimeout(timeId)
}
timeId = setTimeout(() => {
timeId = null
fn.apply(this, args)
}, delay)
}
}