防抖与节流
简单版实现
- 去抖 让一个函数在一定间隔内没有被调用时,才开始执行被调用函数
延迟处理,如果在延时内又触发时间,则重新延迟
js
function debound(func, delay, immediate) {
let timer = null
return function() {
let context = this
let args = arguments
if (timer) clearTimeout(timer)
if (immediate) {
// 根据距离上次触发操作的时间是否到达delay来决定是否要现在执行函数
var doNow = !timer
// 每次都重新设置timer 就是要保证每次执行至少 delay时间后才执行
timer = setTimeout(function() {
timer = null
})
}
}
}- 节流 让一个函数无法在很段时间内连续调用,当上一次函数执行后过了规定时间间隔,才能进行下一次调用
js
/**
* @param {Function} handler 要进行节流的函数
* @param {Number} wait 需要等待的时间
* @return {Function}
*/
function throttle(handler, wait) {
let lastTime = 0
return function() {
let curTime = +new Date()
if (curTime - lastTime > wait) {
handler.apply(this, arguments)
lastTime = curTime
}
}
}防抖
在滚动事件中需要做复杂计算或者实现按钮防多次点击操作
这些需求都可用防抖来实现。若在频繁事件回调中做复杂计算,很可能导致页面卡顿,不如将多次计算合并为一次,只在一个精确点做操作。防抖轮子很多,这里使用underscore源码来解释防抖:
js
/**
* 返回函数连续调用时,空闲时间必须大于或等于 wait ~ func才会执行
* @param {function} func 回调
* @param {number} wait 等待时间
* @param {boolean} immediate 设为true时,是否立即调用函数
* @return {function} 返回用户调用函数
*/
_.debounce = function(func, wait, immediate) {
var timeout, args, context, timestamp, result
var later = function() {
// 现在和上一次时间戳比较
var last = _.now() - timestamp
// 如果当前间隔时间少于设定时间 且 大于0就重新设置定时器
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last)
} else {
// 否则就是时间到了 执行回调函数
timeout = null
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function() {
context = this
args = arguments
// 获得时间戳
timestamp = _.now()
// 如果定时器不存在 且立即执行函数
var callNow = immediate && !timeout
// 如果定时器不存在就创建一个
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
// 如果需要立即执行函数的话,通过apply执行
result = func.apply(context, apply)
context = args = null
}
return result
}
}整体函数实现不难,总结一下
- 对于按钮防点击的实现: 一旦开始一个定时器,只要定时器还在,不管怎么点击都不会执行回调。一旦定时器结束并设置为null,就可以再次点击了
- 对于延迟执行函数来说的实现:每次调用防抖函数都会判断本次调用和之前的时间间隔,如果小于需要调用的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到,就会执行相应的回调
节流
防抖动和节流本质是不一样的。 防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每间隔一段时间执行
js
/**
* 返回函数连续调用时, func执行频率限定为 次/wait
* @param {function} func 回调
* @param {number} wait 等待时间
* @param {boolean} options 若想忽略开始函数调用,传入{leading: false}
* 若想忽略结尾函数调用,传入{trailing: false}
* 两者不能共存,否则函数不执行
* @return {function} 返回用户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result
var timeout = null
// 之前的时间戳
var previous = 0
// 如果options没传则设为空对象
if (!options) options = {}
// 定时器回调函数
var later = function() {
// 如果设置了leading 就将previous 设为0
// 用于下面函数的第一个if判断
previous = options.leading === false ? 0 : _.now()
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null
result = func.apply(context, args)
if (!timeout) context = args = null
}
return function() {
// 获得当前时间戳
var now = _.now()
// 首次进入前者肯定为true 若需要第一个不执行函数,
// 就将上次时间戳设为当前的,这样在计算remaining的值时会大于0
if (!previous && options.leading === false) previous = now
// 计算剩余时间
context = this
args = arguments
// 若当前调用已大于上次调用时间 + wait 或者用户手动调了时间
// 若设置了trailing 只会进入这条件
// 若没有设置leading,那么第一次会进入该条件
// (定时器的延迟不一定准,有可能进入该条件)
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉,否则调用二次回调
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和trailing
// 没有就开启一个定时器,且不能同时设置leading和trailing
timeout = setTimeout(later, remaining)
}
return result
}
}