概念理解
防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
说起防抖大家肯定会想到节流,着两个就跟一对双胞胎一样,让大家经常傻傻搞不清楚
我们先来看一下节流的概念
节流就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率
我拿电梯关门举个例子吧:
防抖
你按了电梯关门按钮,电梯还有三秒要关闭,在你要关闭前的1.5s,按了一次开门按钮电梯将会重新将要关闭时间重置为3秒
节流
你按了电梯关门按钮,在电梯将要关闭的三秒内,你再怎么按电梯按钮都不会响应
常用情况
防抖
在前端开发中会遇到一些频繁的事件触发,比如:
mousedown、mousemove
keyup、keydown
节流
在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
防抖实现方式
第一版
function debounce(func, wait) {
return function () {
let timer
clearTimeout(timer)
timer = setTimeout(() => {
func
}, wait)
}
}
这个是最简单的一版debounce的实现方式,但是缺点有许多,我们先来看第一个
一,this 指向问题
我们可知在setTimeout 中this 指向windows 详情可 MDN看详细说明
第二版(修改this问题)
function debounce(func, wait) {
return function () {
let timer
const contentThis = this
clearTImeout(timer)
timer = setTimeout(() => {
func.apply(contentThis)
}, wait)
}
}
第二版中,this指向是正常情况,但是如果func 有参数的话,就会导致参数的丢失,所以我们开启第三版
第三版(参数丢失问题)
function debounce(func, wait) {
return function () {
let timer
const contentThis = this
let arg = arguments
clearTImeout(timer)
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
}
第三版完后,大家会发现还有一个问题,这个debounce 并不能立即执行,下面我们再改一下代码
function debounce(func, wait, immediate) {
return function () {
let timer = null
const _this = this
let arg = arguments
if (timer) clearTImeout(timer)
if (immediate) {
let runNow = !tiemr
timer = setTimeout(function () {
timer = null
}, wait)
if (runNow) {
func.apply(_this, arg)
}
} else {
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
}
}
我们还需要注意一种情况,当func 函数有返回值时我们需要将返回值带上,但是会有一个问题,当setTimeOut this指向的问题,导致再延时器中返回的数据只能是undefined,所以我们只能再immediate上添加
第四版(返回值问题)
function debounce(func, wait, immediate) {
return function () {
let result
let timer = null
const _this = this
let arg = arguments
if (timer) clearTImeout(timer)
if (immediate) {
let runNow = !tiemr
timer = setTimeout(function () {
timer = null
}, wait)
if (runNow) {
result = func.apply(_this, arg)
}
} else {
timer = setTimeout(() => {
func.apply(contentThis, arg)
}, wait)
}
return result
}
}
第五版(添加防抖取消代码)
_.debounce = function (func, wait, immediate) {
var timeout, result;
var later = function (context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArgs(function (args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
节流的实现方式
节流的原理我们在上面已经说过了,每隔一段时间就只执行一次事件
我们先看第一种方式
第一版(时间戳版)
function throttle(func, wait) {
let previous = 0
return function () {
let now = +new Date()
let _this = this
let arg = arguments
if (now - previous > wait) {
func.apply(_this, arg)
previous = now
}
}
}
使用方式如下
content.onmousemove = throttle(count,1000);
第二版(定时器版)
function throttle(func, wait) {
return function () {
let timer = null
let arg = arguments
if (!timer) {
timer = setTimeout(() => {
timer = null
func.apply(context, arg)
}, wait)
}
}
}
现在我们分析一下,上面两版
第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
所以我们将这两版总结一下,实现一版,鼠标移入能立即执行,但是停止触发的时候还能再次执行一次,代码如下:
第三版
function throttle(func, wait) {
let previous = 0
let later = function () {
previous = +new Date()
tiemr = null
func.apply(_this, arg)
}
let throttled = function () {
let now = +new Date()
let remaining = wait - (now - previous)
let _this = this
let arg = arguments
let tiemr = null
if (remaining <= 0 || remaining > wait) {
if (tiemr) {
clearTimeout(tiemr)
timer = null
}
previous = now
func.apply(_this, arg)
} else if (!tiemr) {
tiemr = setTimeout(later, remaining)
}
}
return throttled
}
上面的代码还有许多局限,比如说,我想要实现,鼠标移入不执行,但是停止触发的时候再执行一次的效果,或者鼠标移入立即执行,但是停止触发的时候不再执行的效果。对于这种情况我们应该如何去做呢?
那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调
我们来优化一下代码
代码
function throttle(func, wait, options) {
let previous = 0
if (!options) {
options = {}
}
let later = function () {
previous = options.leading === false ? 0 : new Date().getTime()
tiemr = null
func.apply(_this, arg)
if (!timeout) _this = args = null
}
let throttled = function () {
let now = new Date().getTime()
if (!previous && options.leading === false) previous = now
let remaining = wait - (now - previous)
let _this = this
arg = arguments
if (remaining <= 0 || remaining > wait) {
if (tiemr) {
clearTimeout(timer)
timer = null
}
previous = now
func.apply(_this, arg)
if (!tiemr) _this = arg = null
} else if (!tiemr && options.trailing !== false) {
timer = setTimeout(later, remaining)
}
}
return throttled
}
注意
我们要注意有这样一个问题:
那就是 leading:false 和 trailing: false 不能同时设置。
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});
以上就完成了函数防抖和节流的实现
react hook 下的防抖和节流
最近我在react hook 中使用防抖和节流发现不起作用,最后发现是自己没有掌握react hook 的机制导致的
当组件组件重新渲染的时候,hooks 会重新执行一遍,这样debounce高阶函数里面的timer就不能起到缓存的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破坏了。
所以我们可以利用React组件的缓存机制重新实现一个react hook 版的防抖和节流
react hook 防抖
import * as React from 'react'
export function useDebounce(fn, delay, dep = []) {
const { current } = React.useRef({ fn, timer: null })
React.useEffect(
function () {
current.fn = fn
},
[fn]
)
return React.useCallback(function f(...args) {
if (current.timer) {
clearTimeout(current.timer)
}
current.timer = setTimeout(() => {
current.fn.call(this, ...args)
}, delay)
}, dep)
}
react hook 节流
import * as React from 'react'
export function useThrottle(fn, delay, dep = []) {
const { current } = React.useRef({ fn, timer: null })
React.useEffect(
function () {
current.fn = fn
},
[fn]
)
return React.useCallback(function f(...args) {
if (!current.timer) {
current.timer = setTimeout(() => {
delete current.timer
}, delay)
current.fn.call(this, ...args)
}
}, dep)
}
react 使用防抖和节流的注意点
我在实现防抖的时候还遇到遇到一个问题,我再这一起记录一下吧
当我在实现react中触发input 组件中的onChange 事件时去发送一个异步请求,但是发现一直获取不到value 值,代码如下
const onSearchChange = (e) => {
e.persist()
e.preventDefault()
e.stopPropagation()
setSearchValue(e.currentTarget.value)
useDebounce((e) => {
setFilterValue(e.currentTarget.value)
}, 500)
}
就是setFilterValue 一直获取不到值,最后经过调查发现了原由,
首先,在 onSearchChange 中,事件触发,就能获取到 event 对象,其中主要就是 event.target 就是当前触发事件的 dom 对象,由于 useDebounce延迟执行,导致了 onSearchChange 函数已经执行完了,进入了 react-dom 中相关一系列操作(进行了一系列复杂的操作),下面给出最关键的 executeDispatchesAndRelease,executeDispatchesAndRelease 方法释放 event.target 的值
/**
* Dispatches an event and releases it back into the pool, unless persistent.
*
* @param {?object} event Synthetic event to be dispatched.
* @private
*/
var executeDispatchesAndRelease = function(event) {
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
由于 event 在 useDebounce中作为了参数,内存中没有清除,执行上面的方法 event.target = null; event 为引用类型,一处改变,所有用到的地方都会改变。导致了 useDebounce中 event 也发生了变化。
解决办法:
const onSearchChange = (e) => {
e.persist()
e.preventDefault()
e.stopPropagation()
setSearchValue(e.currentTarget.value)
handleSearchChange(e.currentTarget.value)
}
const handleSearchChange = useDebounce((value) => {
setFilterValue(value)
}, 500)
参考文档:
JavaScript专题之跟着 underscore 学节流
React hooks 怎样做防抖?
来源:oschina
链接:https://my.oschina.net/u/4310671/blog/4658174