有以下代码git
let n = 1
window.onmousemove = () => {
console.log(`第${n}次触发回调`)
n++
}
复制代码
当咱们在PC端页面上滑动鼠标时,一秒能够能够触发约60次事件。你们也能够访问下面的在线例子进行测试。github
查看在线例子: 函数节流-监听鼠标移动触发次数测试 by Logan (@logan70) on CodePen.浏览器
这里的回调函数只是打印字符串,若是回调函数更加复杂,可想而知浏览器的压力会很是大,可能下降用户体验。app
resize
、scroll
或mousemove
等事件的监听回调会被频繁触发,所以咱们要对其进行限制。async
函数节流简单来讲就是对于连续的函数调用,每间隔一段时间,只让其执行一次。初步的实现思路有两种:函数
设置一个对比时间戳,触发事件时,使用当前时间戳减去对比时间戳,若是差值大于设定的间隔时间,则执行函数,并用当前时间戳替换对比时间戳;若是差值小于设定的间隔时间,则不执行函数。post
function throttle(method, wait) {
// 对比时间戳,初始化为0则首次触发当即执行,初始化为当前时间戳则wait毫秒后触发才会执行
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 间隔大于wait则执行method并更新对比时间戳
if (now - previous > wait) {
method.apply(context, args)
previous = now
}
}
}
复制代码
查看在线例子: 函数节流-初步实现之时间戳 by Logan (@logan70) on CodePen.测试
当首次触发事件时,设置定时器,wait毫秒后执行函数并将定时器置为null
,以后触发事件时,若是定时器存在则不执行,若是定时器不存在则再次设置定时器。优化
function throttle(method, wait) {
let timeout
return function(...args) {
let context = this
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
method.apply(context, args)
}, wait)
}
}
}
复制代码
查看在线例子: 函数节流-初步实现之定时器 by Logan (@logan70) on CodePen.ui
mousemove
)mousedown/keydown
事件(单位时间只能发射一颗子弹)mousemove
)mousemove
)keyup
)scroll
加了 debounce
后,只有用户中止滚动后,才会判断是否到了页面底部;若是是 throttle
的话,只要页面滚动就会间隔一段时间判断一次代码说话,有错恳请指出
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result 记录method的执行返回值
let timeout, result
// 记录上次原函数执行的时间(非每次更新)
let methodPrevious = 0
// 记录上次回调触发时间(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
// 使用Promise,能够在触发回调时拿到原函数执行的返回值
return new Promise(resolve => {
let now = new Date().getTime()
// 两次相邻触发的间隔
let interval = now - throttledPrevious
// 更新本次触发时间供下次使用
throttledPrevious = now
// 重置methodPrevious为now,remaining = wait > 0,伪装刚执行过,实现禁止当即执行
// 统一条件:leading为false
// 加上如下条件之一
// 1. 首次触发(此时methodPrevious为0)
// 2. trailing为true时,中止触发时间超过wait,定时器内函数执行(methodPrevious被置为0),而后再次触发
// 3. trailing为false时(不设定时器,methodPrevious不会被置为0),中止触发时间超过wait后再次触发(interval > wait)
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
// 保险起见,清除定时器并置为null
// 伪装刚执行过要伪装的完全XD
if (timeout) {
clearTimeout(timeout)
timeout = null
}
}
// 距离下次执行原函数的间隔
let remaining = wait - (now - methodPrevious)
// 1. leading为true时,首次触发就当即执行
// 2. 到达下次执行原函数时间
// 3. 修改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新对比时间戳,执行函数并记录返回值,传给resolve
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
// leading为false时将methodPrevious设为0的目的在于
// 若不将methodPrevious设为0,若是定时器触发后很长时间没有触发回调
// 下次触发时的remaining为负,原函数会当即执行,违反了leading为false的设定
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
// 加入取消功能,使用方法以下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
复制代码
调用节流后的函数的外层函数也须要使用Async/Await语法等待执行结果返回
使用方法见代码:
function square(num) {
return Math.pow(num, 2)
}
// let throttledFn = throttle(square, 1000)
// let throttledFn = throttle(square, 1000, {leading: false})
// let throttledFn = throttle(square, 1000, {trailing: false})
let throttledFn = throttle(square, 1000, {leading: false, trailing: false})
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函数不执行时val为undefined
if (typeof val !== 'undefined') {
console.log(`原函数返回值为${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠标移动时,每间隔1S输出:
// 原函数的返回值为:16
复制代码
查看在线例子: 函数节流-最终版 by Logan (@logan70) on CodePen.
具体的实现步骤请往下看
这样实现的效果是首次触发当即执行,中止触发后会再执行一次
function throttle(method, wait) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// 距离下次函数执行的剩余时间
let remaining = wait - (now - previous)
// 若是无剩余时间或系统时间被修改
if (remaining <= 0 || remaining > wait) {
// 若是定时器还存在则清除并置为null
if (timeout) {
clearTimeout(timeout)
timeout = null
}
// 更新对比时间戳并执行函数
previous = now
method.apply(context, args)
} else if (!timeout) {
// 若是有剩余时间但定时器不存在,则设置定时器
// remaining毫秒后执行函数、更新对比时间戳
// 并将定时器置为null
timeout = setTimeout(() => {
previous = new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
复制代码
咱们来捋一捋,假设连续触发回调:
查看在线例子: 函数节流-优化初版:融合两种实现方式 by Logan (@logan70) on CodePen.
// leading为控制首次触发时是否当即执行函数的配置项
function throttle(method, wait, leading = true) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
// !previous表明首次触发或定时器触发后的首次触发,若不须要当即执行则将previous更新为now
// 这样remaining = wait > 0,则不会当即执行,而是设定定时器
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout) {
timeout = setTimeout(() => {
// 若是leading为false,则将previous设为0,
// 下次触发时会与下次触发时的now同步,达到首次触发(对于用户来讲)不当即执行
// 若是直接设为当前时间戳,若中止触发一段时间,下次触发时的remaining为负值,会当即执行
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
复制代码
查看在线例子: 函数节流-优化第二版:提供首次触发时是否当即执行的配置项 by Logan (@logan70) on CodePen.
// trailing为控制中止触发后是否还执行一次的配置项
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
return function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
// 若是有剩余时间但定时器不存在,且trailing不为false,则设置定时器
// trailing为false时等同于只使用时间戳来实现节流
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
}
复制代码
查看在线例子: 函数节流-优化第三版:提供中止触发后是否还执行一次的配置项 by Logan (@logan70) on CodePen.
有些时候咱们须要在不可触发的这段时间内可以手动取消节流,代码实现以下:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout
let previous = 0
// 将返回的匿名函数赋值给throttled,以便在其上添加取消方法
let throttled = function(...args) {
let context = this
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
method.apply(context, args)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
method.apply(context, args)
}, remaining)
}
}
// 加入取消功能,使用方法以下
// let throttledFn = throttle(otherFn)
// throttledFn.cancel()
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
// 将节流后函数返回
return throttled
}
复制代码
查看在线例子: 函数节流-优化第四版:提供取消功能 by Logan (@logan70) on CodePen.
须要节流的函数多是存在返回值的,咱们要对这种状况进行处理,underscore
的处理方法是将函数返回值在返回的debounced
函数内再次返回,可是这样实际上是有问题的。若是原函数执行在setTimeout
内,则没法同步拿到返回值,咱们使用Promise处理原函数返回值。
function throttle(method, wait, {leading = true, trailing = true} = {}) {
// result记录原函数执行结果
let timeout, result
let previous = 0
let throttled = function(...args) {
let context = this
// 返回一个Promise,以即可以使用then或者Async/Await语法拿到原函数返回值
return new Promise(resolve => {
let now = new Date().getTime()
if (!previous && leading === false) previous = now
let remaining = wait - (now - previous)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = method.apply(context, args)
// 将函数执行返回值传给resolve
resolve(result)
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
previous = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
// 将函数执行返回值传给resolve
resolve(result)
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
复制代码
使用方法一:在调用节流后的函数时,使用then
拿到原函数的返回值
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000, false)
window.onmousemove = () => {
throttledFn(4).then(val => {
console.log(`原函数的返回值为:${val}`)
})
}
// 鼠标移动时,每间隔1S后输出:
// 原函数的返回值为:16
复制代码
使用方法二:调用节流后的函数的外层函数使用Async/Await语法等待执行结果返回
使用方法见代码:
function square(num) {
return Math.pow(num, 2)
}
let throttledFn = throttle(square, 1000)
window.onmousemove = async () => {
try {
let val = await throttledFn(4)
// 原函数不执行时val为undefined
if (typeof val !== 'undefined') {
console.log(`原函数返回值为${val}`)
}
} catch (err) {
console.error(err)
}
}
// 鼠标移动时,每间隔1S输出:
// 原函数的返回值为:16
复制代码
查看在线例子: 函数节流-优化第五版:处理原函数返回值 by Logan (@logan70) on CodePen.
模仿underscore
实现的函数节流有一点美中不足,那就是 leading:false
和 trailing: false
不能同时设置。
若是同时设置的话,好比当你将鼠标移出的时候,由于 trailing
设置为 false
,中止触发的时候不会设置定时器,因此只要再过了设置的时间,再移入的话,remaining
为负数,就会马上执行,就违反了 leading: false
,这里咱们优化的思路以下:
计算连续两次触发回调的时间间隔,若是大于设定的间隔值时,重置对比时间戳为当前时间戳,这样就至关于回到了首次触发,达到禁止首次触发(伪)当即执行的效果,代码以下,有错恳请指出:
function throttle(method, wait, {leading = true, trailing = true} = {}) {
let timeout, result
let methodPrevious = 0
// 记录上次回调触发时间(每次都更新)
let throttledPrevious = 0
let throttled = function(...args) {
let context = this
return new Promise(resolve => {
let now = new Date().getTime()
// 两次触发的间隔
let interval = now - throttledPrevious
// 更新本次触发时间供下次使用
throttledPrevious = now
// 更改条件,两次间隔时间大于wait且leading为false时也重置methodPrevious,实现禁止当即执行
if (leading === false && (!methodPrevious || interval > wait)) {
methodPrevious = now
}
let remaining = wait - (now - methodPrevious)
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
methodPrevious = now
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
} else if (!timeout && trailing !== false) {
timeout = setTimeout(() => {
methodPrevious = leading === false ? 0 : new Date().getTime()
timeout = null
result = method.apply(context, args)
resolve(result)
// 解除引用,防止内存泄漏
if (!timeout) context = args = null
}, remaining)
}
})
}
throttled.cancel = function() {
clearTimeout(timeout)
methodPrevious = 0
timeout = null
}
return throttled
}
复制代码
查看在线例子: 函数节流-优化第六版:可同时禁用当即执行和后置执行 by Logan (@logan70) on CodePen.
JavaScript专题之跟着 underscore 学节流
若是有错误或者不严谨的地方,请务必给予指正,十分感谢。