Appearance
函数节流(throttle)
函数执行一次后,只有大于规定的时间后才会执行第二次, 适合多次事件按时间做平均分配触 发,比如 1s 内我们可以点击鼠标多次, 设置 wait 时间为 2s, 就算我们不停点击鼠标 10s, 也只会触发 6 次。
应用场景:
1.窗口调整(resize)、页面滚动(scroll)
2.DOM元素的拖拽(mousemove)
3.抢购疯狂点击(click)
javascript
function throttle (fn, wait) {
let prev = 0
return function (...args) {
let now = Date.now()
if (now - prev > wait) {
const result = func.apply(context, args)
prev = now
return result
}
}
}javascript
/**
* 函数节流
*
* @param {Function} fn - 需要进行节流处理的原函数
* @param {number} wait - 节流的时间间隔
* @param {Object} options - 用于设置开始边界和结束边界是否触发的配置项
* @param {boolean} options.leading - 开始边界是否触发
* @param {boolean} options.trailing - 结束边界是否触发
* @returns {Function} - 生成的节流函数
*/
const throttle = (fn, wait = 200, options = {}) => {
const { leading, trailing } = options
let context = null,
parmas = null,
result = null,
timer = null,
prev = 0
// 结束边界触发的函数
const later = () => {
// 之后再次触发时当作第一次点击,如果开始边界要不触发,应该让 prev 为 0。
prev = leading
? Date.now()
: 0
timer = null
result = fn.call(context, ...parmas)
if (!timer) {
context = parmas = null
}
}
const _throttle = function (...args) {
parmas = args
context = this
// 记录点击的时间
const now = Date.now()
// 开始边界不触发,本来需要用一个变量标识是否是第一次点击,由于 prev 只有初始时的
// 值是 0,正好可以用来标识是否是第一次点击。
if (!prev && !leading) {
prev = now
}
// 计算需要等待的时间
const remaining = wait - (now - prev)
if (remaining <= 0 || remaining > wait) { // remaining > wait 代表修改了时间
// 清除之前因结束边界触发而设置的定时器,这种情况只会在结束边界定时器回调即将触
// 发的同时又点击了一下时出现,此时正好超过等待时间。如果因时间误差先执行了点击
// 时的代码,就会取消掉定时器回调;如果因时间误差先执行了定时器回调,prev 会更
// 新,就不会执行下面的代码。总之保证了规定时间内只会执行一次。
if (timer) {
clearTimeout(timer)
timer = null
}
prev = now
result = fn.call(context, ...parmas)
if (!timer) {
context = parmas = null
}
return result
}
// 结束边界触发
if (!timer && trailing) {
timer = setTimeout(later, remaining)
return result
}
}
// 取消
_throttle.cancle = () => {
clearTimeout(timer)
prev = 0
timer = context = parmas = null
}
return _throttle
}javascript
const now = _.now()
const foo = () => {
console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000) // true false
setTimeout(() => {
bar() // 立即触发,因为默认开始边界触发。
}, 0)
setTimeout(() => {
bar() // 不触发,还没经过 1000 ms
}, 500)
setTimeout(() => {
bar() // 1200 ms 时立即触发
}, 1200)
setTimeout(() => {
bar() // 不触发
}, 1500)javascript
const now = _.now()
const foo = () => {
console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { leading: false }) // false false
setTimeout(() => {
bar() // 不触发,因为设置了开始边界不触发。
}, 0)
setTimeout(() => {
bar() // 不触发,还没经过 1000 ms
}, 500)
setTimeout(() => {
// 1200 ms 时触发,不会被当作新的开始来处理,只有开启结束边界触发时才会重置 prev。
bar()
}, 1200)
setTimeout(() => {
bar() // 不触发,距离上次触发还没经过 1000 ms
}, 1500)javascript
const now = _.now()
const foo = () => {
console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { trailing: true }) // true true
setTimeout(() => {
bar() // 触发,因为设置了开始边界触发。
}, 0)
setTimeout(() => {
bar() // 1000 ms 时触发,因为设置了结束边界触发
}, 500)
setTimeout(() => {
// 被设置成了结束边界触发
bar()
}, 1200)
setTimeout(() => {
bar() // 更新了结束边界触发,2000 ms 触发
}, 1500)
setTimeout(() => {
bar() // // 被设置成了结束边界触发,3000 ms 触发
}, 2600)
setTimeout(() => {
bar() // 距离上次触发超过了 1000ms,被当作了新的开始,4100 ms 触发。
}, 4100)javascript
const now = _.now()
const foo = () => {
console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { leading: false, trailing: true }) // false true
setTimeout(() => {
bar() // 不触发,因为设置了开始边界不触发。
}, 0)
setTimeout(() => {
bar() // 1000 ms 时触发,因为设置了结束边界触发
}, 500)
setTimeout(() => {
// 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
bar()
}, 1200)
setTimeout(() => {
bar() // 2200 ms 触发,因为设置了结束边界触发
}, 1500)
setTimeout(() => {
// 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
bar() // 3600 ms 触发,因为设置了结束边界触发
}, 2600)
setTimeout(() => {
// 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
bar() // 5700 ms 触发,被当作了新的开始。
}, 4700)