throttle节流,debounce防抖
假设时间频率:1s
throttle 是每隔 1s,必然执行。
高铁不能人,到点就发车。
debounce 是触发后必须等待 1s 后才执行,如果等待过程中再次被触发,则重新计时。
电梯来人就感应不关门,直到没人才关闭。
基于此,常见场景:
click
,为了防止连续点击,应设置在点击停止一段时间后,再执行 click 事件。(debounce)scroll
,会执行的很频繁,一般会将 scroll 事件隔一段时间执行。(throttle)mousemove
和 scroll
同理(throttle)change
,一般不用做限制,如果是输入框,得失焦后才触发。其他的下拉框单选框什么的,也不会很频繁的调用。input
,简单表单输入,到也不用做限制。以 throttle-debouncenpm
依赖为例。
import { throttle } from 'throttle-debounce'
function onScroll(event) {
console.log(event)
// ...
}
const _scroll = throttle(200, onScroll)
window.addEventListener('scroll', _scroll)
<template>
<div>
<button @click="debounceClick(false, $event)">debouncebutton>
div>
template>
<script>
import { debounce } from 'throttle-debounce'
export default {
methods: {
debounceClick: debounce(300, function(bool, event) {
console.log(bool, event)
})
}
}
script>
参考1,参考2,也参考了 throttle-debounce^1.0.1 | npm 源码。
基础实现
function debounce(delay, callback) {
let timeoutId = 0
function wrapper() {
let self = this
let args = arguments
function exec() {
callback.apply(self, args)
}
// 只要调用就清理,否则就耐心等待 setTimeout 执行。
clearTimeout(timeoutId)
timeoutId = setTimeout(exec, delay)
}
return wrapper
}
关于
this
的问题,因为this
应指向返回的wrapper
函数。
问题:即便只点一次,也得等 delay
之后才能触发。
可以加个判断,是在开始触发 或 等待 delay
后触发。
function debounce(delay, callback, atBegin) {
let timeoutId = 0
function wrapper() {
let self = this
let args = arguments
function exec() {
callback.apply(self, args)
}
clearTimeout(timeoutId)
if (atBegin) {
let callNow = !timeoutId
timeoutId = setTimeout(function() {
timeoutId = 0
}, delay)
if (callNow) {
exec()
}
} else {
timeoutId = setTimeout(exec, delay)
}
}
return wrapper
}
或是如下写法
function debounce(delay, callback, atBegin) {
let timeoutId = 0
function wrapper() {
let self = this
let args = arguments
function exec() {
callback.apply(self, args)
}
function clear () {
timeoutId = 0;
}
clearTimeout(timeoutId)
if (atBegin && !timeoutId) {
exec()
}
timeoutId = setTimeout(atBegin ? clear : exec, delay)
}
return wrapper
}
function throttle(delay, callback) {
let timeoutId = 0
let lastExec = 0
// 替代后的函数
function wrapper() {
let args = arguments
let self = this
let elapsed = Number(new Date()) - lastExec // 上次执行后经过的时间
function exec() {
lastExec = Number(new Date()) // 记录执行的时间点
callback.apply(self, args)
}
clearTimeout(timeoutId)
if (elapsed > delay) {
exec()
} else {
timeoutId = setTimeout(exec, delay - elapsed)
}
}
return wrapper
}
上面的实现,有个问题就是 trailing
(尾随)效果,也就是 else 是否需要触发。
比如对滚动页面来说,滚动已经停止了,但可能停止的时间点正好是 elapsed < delay
,那再等 delay - elapsed
时间后,还会再触发一次。
只需要加个判断即可。
需要注意的是,npm 依赖的参数 noTrailing
意思正好相反,noTrailing = false
时,添加尾随效果。
function throttle(delay, callback, trailing = false) {
let timeoutId = 0
let lastExec = 0
// 替代后的函数
function wrapper() {
let args = arguments
let self = this
let elapsed = Number(new Date()) - lastExec // 上次执行后经过的时间
function exec() {
lastExec = Number(new Date()) // 记录执行的时间点
callback.apply(self, args)
}
clearTimeout(timeoutId)
if (elapsed > delay) {
exec()
} else if (trailing) {
timeoutId = setTimeout(exec, delay - elapsed)
}
}
return wrapper
}
函数参数顺序问题。在 throttle-debounce^1.0.1 | npm 源码。中,有如下代码:
module.exports = function ( delay, noTrailing, callback, debounceMode ) {
// ...
// `noTrailing` defaults to falsy.
if ( typeof noTrailing !== 'boolean' ) {
debounceMode = callback;
callback = noTrailing;
noTrailing = undefined;
}
// ...
}
原来当某个参数值和想要的不相符时,可以直接替换位置!
以上。