- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第25期,链接:juejin.cn/post/708744…
本来应该是主要分析防抖的,但是平时使用时防抖和节流基本都是一个比较容易混淆的概念,而且经常拿出来进行比较,所以就一起进行分析了。
什么是防抖和节流
防抖和节流都是用来减少函数的执行次数,减轻客户端和服务端压力,提高执行效率与性能并且少浪费资源的。我们为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(防抖)和debounce(节流)的方式来减少调用频率。
定义:
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
看了上面两个的定义之后,其实还是有很大的区别的,所以也有不同的适用范围。
- 防抖适用于输入框远程查询事件,在线文档自动保存,浏览器视口大小改变
- 节流适用于按钮提交事件,页面滚动事件的触发,搜索框联想功能
其实有时候看了上面的两个适用场景,我自己都会混淆,比如输入框这个情况,我自己有时候如果和业务相结合就会出现混淆。
上面已经对防抖和节流做了一定的解释了,那么让我们来针对这两种情况实现一下源码并且比对一下lodash的源码实现吧
防抖
其实实现防抖还是比较简单的,通过上面的定义我们可以知道,在一定时间之后执行该函数所以需要使用setTimeout在经过设定时间之后执行该函数,如果在未执行之前反复触发就要重新计时,那么就需要在未执行之前将之前的执行清除掉,那么就需要使用clearTimeout进行清除的操作,经过上面的分析之后其实代码就很简单了,如下:
function debounce (func, time) {
// 闭包存储计时的id,方便后面删除
var timeout = null;
var debounced = function () {
var _this = this;
var params = arguments;
// 删除之前的定时任务,或进行判断是否有定时器
clearTimeout(timeout);
// 设置计时器
timeout = setTimeout(function () {
func.apply(_this, params)
}, time)
}
return debounced
}
当然我自己实现的是一个很简单的防抖,开源的代码例如lodash会有很多的考虑,比我的复杂的多,但是我们之间的原理是一样的。 看一下lodash是如何实现的吧:
// 判断是否是一个对象
import isObject from './isObject.js'
// 一个全局的变量
import root from './.internal/root.js'
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
// 源码严谨的判断了传入的参数是否是一个函数。
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
// 对时间的处理
wait = +wait || 0
// 判断用户传入的特殊要求是否是一个对象,然后对属性进行处理,这里通过属性就可以实现节流的功能
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 触发函数,就是直接执行用户真正的函数
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 此处是使用setTimeout创建一个定时器,返回一个定时器标志位
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 取消定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
lastInvokeTime = time
timerId = startTimer(timerExpired, wait)
return leading ? invokeFunc(time) : result
}
// 用来计算剩余时间的(针对节流使用)
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 判断是否应该调用
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
// 取消定时器功能
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
// 这个是实现防抖的主要函数
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export default debounce
lodash的函数是比较复杂的,因为里面有很多的配置,通过这些配置来实现更加丰富的功能,节流函数也相当于实现在了这个函数中,但是这里面的原理是和上面一样的。但是如果化繁为简的话就没有那么复杂了。
节流
节流是在第一次执行之后,一段时间之内都不会再次执行此函数,那就直接执行此函数,然后设置一个标志位(timeout)为一个任意值,在此值没有再次恢复到null时是不可以再次执行此函数的,需要立刻停止,那对于何时将timeout变为null,那就需要使用setTimeout了,在指定时间之后将值复位,这样就可以实现节流函数了,所以节流函数的简版如下:
function throttle(func, time) {
var timeout = null;
return function () {
// timeout没有变为null之前不可以执行
if (timeout) {
return;
}
var _this = this;
var params = arguments;
// 立刻执行函数
func.apply(_this, params);
// 在一段时间后将timeout复位
timeout = setTimeout(() => {
timeout = null;
}, time)
}
}
lodash源码如下:
// 引入了防抖函数
import debounce from './debounce.js'
import isObject from './isObject.js'
function throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait
})
}
export default throttle
lodash的节流函数的核心就是防抖函数,因为传入的参数导致防抖函数变成了节流函数,所以我们要是设置的参数和源码中一致,也可以将防抖函数变为节流函数。
总结
上面两个函数其实都是「闭包」、「高阶函数」的应用,在实现之前我又再次去看了一下闭包这个概念,还把js的红皮书里面的相关章节看了一下,又有了一些感触吧,其实经常看一些源码,然后根据源码中遇到的问题在回头去看一下书,真的对自己原来不理解的地方又有了一些新的感悟吧。