js_防抖与节流

防抖(debounce)

防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。

防抖的应用场景

  1. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖。
  2. 调整浏览器窗口大小时,resize次数太过于频繁,造成计算过多,此时需要一次到位,就用到了防抖。
  3. 文本编辑器实时保存,当无任何更改操作一秒后进行保存。

特点

等待某种操作停止后,加以间隔进行操作

  • 持续触发不执行
  • 不触发的一段时间之后再执行

代码实现

代码如下,可以看出来防抖重在清零clearTimeout(timer)

function debounce(f, wait) {
    let timer
    return (...args) => {
        clearTimeout(timer)
        timer = setTimeout(()=>{
            f(...args)
        }, wait)
    }
}

节流(throttle)

节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为1秒发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流(Rate Limit)类似。

节流的应用场景

  1. scroll事件,每隔一秒计算一次位置信息等。
  2. 浏览器播放事件,每隔一秒计算一次进度信息等。
  3. input框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求(也可做防抖)

特点

每等待某种间隔后,进行操作

  • 持续触发并不会执行多次
  • 到一定事件 / 其他间隔(如滑动的高度)再去执行

代码实现

代码如下,可以看出来节流重在开关锁timer=null

function throttle(f, wait) {
    let timer
    return (...args) => {
        if(timer) { return }
        timer = setTimeout(() => {
            f(...args)
            timer = null
        }, wait)
    }
}

如何实现节流防抖

在实际开发的业务场景中,我们更多的会选择成熟的第三方库来达到防抖和节流的效果。目前常用的有:LodashUnderscore.js等,还可以使用计数器来实现节流防抖等。

防抖代码实现

<input name="username" type="text" oninput="checkUsername(this)"/>
<script>
	var handler;
    function checkUsername(it) {
        clearTimeout(handler);
        handler = setTimeout(() => {
            // TODO
        }, 1000)
    }
</script>

上面代码的意思是:如果在1000毫秒内,反复执行checkUsername函数,那么setTimeout就会被反复清楚,并且重复设置一个新的1000毫秒的定时任务。直到你执行checkUsername的前后两次间隔超过1000毫秒,定时任务才有机会执行一次。

节流代码实现

<input name="name" type="text" oninput="checkUsername(this)" />
<script>
	var isRuning = false;
    function checkUsername(it) {
        if(!isRuning) {
            isRuning = true;
            // TODO
            setTimeout(() => {
                isRuning = false;
            }, 1000)
        }
    }
</script>

上面代码的意思是:第一次执行checkUsername函数的时候,!isRuningtrue,执行关键代码,改变isRuningtrue,设置一个1000毫秒后再把isRuning再改为false。那么,当你在1000毫秒内反复执行checkUsername的话,关键代码都不会执行,只有等到定时器执行后,再次执行checkUsername,才会执行关键代码。

Lodash防抖节流源码分析

防抖:Lodash实现防抖的核心思想在于不去频繁管理定时器,而是实现了shouldInvoke来判断是否应该执行func函数,只有在对外提供的cancel方法取消延迟时才取消定时器。

下文在函数执行模块详细介绍了shouldInvoke内部实现逻辑,在定时器开关和入口函数中调用来决定是否应该执行func函数。

基本定义

以下是Lodash实现防抖的整体代码结构,入口函数定义了一些定时器相关和函数执行相关的变量。一共10个变量,其中maxWaittimerIdlastCallTimelastInvokeTimeleadingmaxingtrailing7个时间相关的变量是实现定时器开关和函数执行模块的重要支撑。

import isObject from './isObject.js'
import roor from './internal/root.js'
function debounce(func, wait, options) {
	/**  ===== 基础定义  =====  **/
	let lastArgs, // 上一次执行debounce的arguments
		lastThis, // 上一次的 this
		maxWait, // 最大等待时间,保证大于设置的最大间隔后一定会执行,用于实现节流效果
		result, // 函数 func 执行后的返回值
		timerId, // 定时器ID
		lastCallTime // 上一次调用 debounce 的时间

	let lastInvokeTime = 0 // 上一次执行func的时间,用于实现节流效果
	let leading = false // 延迟前第一次触发
	let maxing = false // 是否设置了最大等待时间maxWait,多用于实现节流效果
	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
	/**
	 * isObject 判断是否是一个对象
	 * function isObject(value) {
	 *   const type = typeof value
	 *   return value != null && (type == 'object' || type == 'function')
	 * }
	 */
	if (isObject(options)) {
		leading = !!options.leading
		maxing = 'maxWait' in options
		// maxWait 取 maxWait 和 wait 中最大值,为实现节流效果,需保证 maxWait 的实际值大于 wait 
		maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
		trailing = 'trailing' in options ? !!options.trailing : trailing
	}


	/** ======  定时器开关 ====== */

	// 设置定时器
	function startTimer(pendingFunc, wait) {}

	// 取消定时器
	function cancelTimer(id) {}

	// 计算仍需等待的时间
	function remainingWait(time) {}

	// 定时器回调
	function timerExpired() {}


	/** ======  函数执行 ====== */

	// 延迟前
	function leadingEdge(time) {}

	// 延迟后回调
	function trailingEdge(time) {}

	// 执行 func 函数
	function invokeFunc(time) {}

	// 判断此时是否应该执行 func 函数
	function shouldInvoke(time) {}


	/** ======  对外回调 ====== */

	// 取消延迟
	function cancel() {}

	// 立即调用
	function flush() {}

	// 判断是否在定时中
	function pending() {}

	// 入口函数
	function debounced(...args) {}
	debounced.cancel = cancel
	debounced.flush = flush
	debounced.pending = pending
	return debounced
}

export default debounce

定时器开关

/** ======  定时器开关 ====== */

// 设置定时器
function startTimer(pendingFunc, wait) {
	if (useRAF) {
		// 没设置 wait 或设置 wait 为 0 时调用 window.requestAnimationFrame()。
		// 要求浏览器在下次重绘之前调用指定的回调函数更新动画
		root.cancelAnimationFrame(timerId)
		return root.requestAnimationFrame(pendingFunc)
	}
	return setTimeout(pendingFunc, wait)
}

// 取消定时器
function cancelTimer(id) {
	if (useRAF) {
		return root.cancelAnimationFrame(id)
	}
	clearTimeout(id)
}

// 计算仍需等待的时间
function remainingWait(time) {
	// 当前时间与上一次调用 debounce 的间隔
	const timeSinceLastCall = time - lastCallTime
	// 当前时间与上一次执行 func 的间隔
	const timeSinceLastInvoke = time - lastInvokeTime
	// 剩余等待时间
	const timeWaiting = wait - timeSinceLastCall

	// 是否设置了最大等待时间 ( 是否设置为节流 )
	// 否:剩余等待时间
	// 是:剩余等待时间 和 当前时间与上一次执行 func 的间隔 中的最小值

	return maxing ?
		Math.min(timeWaiting, maxWait - timeSinceLastInvoke) :
		timeWaiting
}

// 定时器回调
function timerExpired() {
	const time = Date.now()
	// 应该执行 func 函数时,执行延迟后回调
	if (shouldInvoke(time)) {
		return trailingEdge(time)
	}
	// 计算仍需等待的时间,重置定时器
	timerId = startTimer(timerExpired, remainingWait(time))
}

函数执行

/** ======  函数执行 ====== */

// 延迟前
function leadingEdge(time) {
	// 设置上次执行 func 函数的时间
	lastInvokeTime = time
	// 设置定时器
	timerId = startTimer(timerExpired, wait)
	// 如果设置了 leading 则立即执行 func 函数一次
	return leading ? invokeFunc(time) : result
}

// 延迟后回调
function trailingEdge(time) {
	timerId = undefined

	// trailing 延迟后继续触发一次 
	// lastArgs 标记着 debounce 至少执行过一次
	if (trailing && lastArgs) {
		return invokeFunc(time)
	}
	// 重置参数
	lastArgs = lastThis = undefined
	return result
}

// 执行 func 函数
function invokeFunc(time) {
	const args = lastArgs
	const thisArg = lastThis

	lastArgs = lastThis = undefined
	lastInvokeTime = time
	result = func.apply(thisArg, args)
	return result
}

// 判断此时是否应该执行 func 函数
function shouldInvoke(time) {
	// 当前时间与上一次调用 debounce 的间隔
	const timeSinceLastCall = time - lastCallTime
	// 当前时间与上一次执行 func 的间隔
	const timeSinceLastInvoke = time - lastInvokeTime

	// 首次调用
	// 超出等待时间间隔 wait
	// 系统时间发生了变更
	// 超出最长等待时间 maxWait
	return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
		(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}

对外回调

 /** ======  对外回调 ====== */

  // 取消延迟
  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
  }

节流:Lodash中节流函数的实现简洁,直接调用防抖函数,通过设置入参的maxWait达到节流效果。

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

总结

防抖:防止抖动,不触发的一段时间之后再执行。代码实现重在清零clearTimeout

节流:控制流量,单位时间内事件只能触发一次,如果服务器端的限流即Rate Limit。代码实现重在开关锁timer = timeout; timer = null

借用防抖和节流的思想,来控制函数执行的时机,可以节约性能,避免页面卡断等带来不好的用户体验。防抖和节流的概念相似不易区分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Joyce Lee

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值