1. 防抖(debounce) 防抖是限制频率,多次出发一次执行。
2. 节流(throttle) 节流是限制次数,限制规定时间内执行次数。
这篇文章以 underscore.js 工具库中的 防抖和节流函数作为演示,并探究其源码。
underscore.js 中文文档
本篇文章只对其原理进行探究,使用方式不做太多概述,如果不知道怎么使用的同学,可自行百度。
**下边是 html 演示代码, 里边引入debounce.js throttle.js 两个文件 **
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖 节流title>
<style>
#container {
width: 100%;
height: 400px;
background: #999;
text-align: center;
line-height: 400px;
color: #fff;
}
style>
head>
<body>
<div id="container">div>
<div id="btn">取消div>
<script src="https://cdn.bootcdn.net/ajax/libs/underscore.js/1.9.1/underscore.js">script>
<script src="./debounce.js">script>
<script src="./throttle.js">script>
body>
html>
效果如下
通过对 container
元素添加 onmousemove
事件,来测试我们实现的这两个方法。左下角的取消按钮用来实现取消操作
let count = 0
let container = document.querySelector('#container')
let btn = document.querySelector('#btn')
function doSomeThing (e) {
console.log(e)
console.log(this)
// 可能会做回调或者 ajax 请求
container.innerHTML = count++
return '我有返回值'
}
container.onmousemove = debounce(doSomeThing, 300, false)
debounce 方法接受三个参数,第一:要被限制防抖的函数,第二:时间间隔,第三:是否立即执行,
- 默认为 false 延迟执行,第一次触发不会执行,规定时间内连续触发也不会执行,停止触发以后在规定时间执行一次,
- true 就是onmousemove事件被触发以后立即执行,在间隔时间内连续触发不会执行,停止触发也不会再次执行,从新触发,还会立即执行。然后在开始计算时间间隔
要思考的问题:
- 防抖节流函数都是高阶函数,会返回一个函数
- 函数内部 this 指向问题,可以用 apply call bind 这些方法, 或者使用箭头函数
- event 对象的问题,涉及到 dom 事件的,有可能会使用 event 对象
- 函数有返回值的问题
根据上边的使用方式执行原理,和 问题,来完成 debounce 函数
function debounce (func, wait, immediate) {
// timeout 是定时器对象, args 对应返回函数的 实参列表 argument, result 是被限制防抖函数中的返回值
let timeout, args, result;
return function () {
// 获取 this 指向存起来,下边定时器中的 func 函数被 apply 调用时用来改变 this 执行,或者定时器函数中直接使用箭头函数
let context = this
// 接收实参列表,如果涉及到 dom 操作,可能会用到 event 对象,这里用来接收
args = arguments
// 函数被触发,先取消定时器,一直触发一直清空,只有等停止触发以后,最后一次清空以后,才会执行下边的代码
clearTimeout(timeout)
// 立即执行,然后在规定时间内不再触发
if (immediate) {
// 第一次触发,timeout = undefined, 取反即为 true, 下边会立即执行,再次触发, timeout已经有值,取反即为 false,下边便不会在立即执行
let callNow = !timeout
// 规定时间到了以后,timeout 置空,再次触发,上边 timeout 取反 即为 true, 下边再次执行
timeout = setTimeout(() => {
timeout = null
}, wait)
// callNow === true 立即执行
if (callNow) {
result = func.apply(context, args)
}
} else {
// 延迟执行,直接延迟规定时间再执行
timeout = setTimeout(function () {
result = func.apply(context, args)
}, wait)
}
// 考虑到如果被限制防抖函数中有返回值的情况,这里直接 return 返回值
return result
}
}
根据上边的代码来讲一下实现原理:
immediate: false 是延迟执行,onmousemove 触发以后 debounce 函数被触发,会返回一个函数,这就是一个高阶函数,函数内部,会先取消定时器,连续触发的情况下就会连续清空,下边的延迟执行方法就会不会执行。只有等到 onmousemove 停止触发以后,最后会执行到下边的延迟函数,等传入的时间间隔到了以后,通过 apply(context, args) 调用一次 被传入的 func (doSomeThing) 函数。
context 是为了防止调用 debounce 函数时this指向被改变,args 是 arguments 对象的实参列表,
里边会有 doSomeThing(event) 函数中的参数,比如 event 对象,可以防止 event 对象丢失。
immediate: true 是立即执行,当 onmousemove 触发以后,debounce 函数被触发,这时候不会是延迟执行,还是先取消掉定时器,第一次触发,timeout = undefined, 取反即为 true, 下边会立即执行,再次触发, timeout已经有值,取反即为 false,下边便不会在立即执行。规定时间到了以后,timeout 置空,再次触发,上边 timeout 取反 即为 true, 下边再次执行
基本上原理和思路就是这样。但是还少一个取消的方法。再来完善一下
完整版本
// 防抖函数
function debounce (func, wait, immediate) {
// timeout 是定时器对象, args 对应返回函数的 实参列表 argument, result 是被限制防抖函数中的返回值
let timeout, args, result;
// 声明 debounced 对象来接收返回函数
let debounced = function () {
// 获取 this 指向存起来,下边定时器中的 func 函数被 apply 调用时用来改变 this 执行,或者定时器函数中直接使用箭头函数
let context = this
// 接收实参列表,如果涉及到 dom 操作,可能会用到 event 对象,这里用来接收
args = arguments
// 函数被触发,先清空定时器,一直触发一直清空,只有等停止触发以后,最后一次清空以后,才会执行下边的代码
clearTimeout(timeout)
// 立即执行,然后在规定时间内不再触发
if (immediate) {
// 第一次触发,timeout = undefined, 取反即为 true, 下边会立即执行,再次触发, timeout已经有值,取反即为 false,下边便不会在立即执行
let callNow = !timeout
// 规定时间到了以后,timeout 置空,再次触发,上边 timeout 取反 即为 true, 下边再次执行
timeout = setTimeout(() => {
timeout = null
}, wait)
// callNow === true 立即执行
if (callNow) {
result = func.apply(context, args)
}
} else {
// 延迟执行,直接延迟规定时间再执行
timeout = setTimeout(function () {
result = func.apply(context, args)
}, wait)
}
// 考虑到如果被限制防抖函数中有返回值的情况,这里直接 return 返回值
return result
}
// 在 debounced 对象上添加 cancel 方法取消定时器,并置空
debounced.cancel = function () {
clearTimeout(timeout)
timeout = null
}
return debounced
}
// 测试示例
let count = 0
let container = document.querySelector('#container')
let btn = document.querySelector('#btn')
let doSome = debounce(doSomeThing, 1000)
btn.onclick = function () {
doSome.cancel()
}
function doSomeThing (e) {
console.log(e)
console.log(this)
container.innerHTML = count++
return '我有返回值'
}
// container.onmousemove = _.debounce(doSomeThing, 300, true)
// container.onmousemove = debounce(doSomeThing, 300, true)
container.onmousemove = doSome
// container.onmousemove = debounce(doSomeThing, 300, false)
防抖使用场景:
1.scroll 事件滚动触发, 上拉加载
2.搜索框输入查询
3.表单验证
4.浏览器窗口缩放, resize 事件
节流方法,根据 underscore.js 中定义的 throttle 和使用使用方法。throttle 方法也是有第三个参数 options 对象这个对象中有两个属性: leading trailing:
如果你想禁用第一次首先执行的话,传递{leading: false},
还有如果你想禁用最后一次执行的话,传递{trailing: false}
两个参数可以同时传递,但是只能有三种情况
第一次会输出,最后一次不会被调用 {leading: true, trailing: false}
第一次不会输出, 最后一次会被调用 {leading: false, trailing: true}
第一次会输出, 最后一次会被调用 {leading: true, trailing: true}
都为 false 的时候 trailing 会失效
带着上边的问题来实现:
第一版:利用时间间隔来实现第一次执行,最后一次不执行
判断条件就是,声明一个旧的时间戳默认值是0 每次触发时拿到当前的时间戳,
如果当前时间戳 减去旧的时间戳 大于 传入的时间间隔就立即执行,然后把 当前时间戳的值赋值给 旧的时间戳
代码如下
function throttle (func, wait) {
let context, args, old = 0;
return function () {
context = this
args = arguments
let now = Date.now()
// 利用时间间隔,实现第一次执行,最后一次不执行
if (now - old > wait) {
func.apply(context, args)
old = now
}
}
}
第二版:第一次不会触发,最后一次会触发,利用定时器延迟实现,第一次不会触发,最后一次会触发
function throttle (func, wait) {
let context, args, timeout;
return function () {
context = this
args = arguments
// 利用定时器延迟执行,可以实现第一次不执行,最后一次执行
if (!timeout) {
timeout = setTimeout(() => {
func.apply(context, args)
clearTimeout(timeout)
timeout = null
}, wait)
}
}
}
第三版:第一次会执行,最后一次也会执行
function throttle (func, wait) {
let context, args, timeout, old = 0;
return function () {
context = this
args = arguments
let now = Date.now()
// 第一次触发会走这里
if (now - old > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
func.apply(context, args)
old = now
// 事件停止触发,最后一次走这里
} else if (!timeout) {
timeout = setTimeout(() => {
// 这里给 old 重新赋值, 不然会因为上边的 now-old > wait 的误差 出现bug
old = Date.now()
timeout = null
func.apply(context, args)
}, wait)
}
}
}
第四版:完整版,有参数版本
function throttle (func, wait, options) {
let context, args, timeout, old = 0;
if (!options) options = {}
let throttled = function () {
context = this
args = arguments
let now = Date.now()
// 第一次不会触发的时候,这里直接把 now 赋值给 old, 下边判断条件就会失效第一次不会执行
if (options.leading === false && !old) {
old = now
}
// 第一次触发会走这里
if (now - old > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
func.apply(context, args)
old = now
// 事件停止触发,最后一次走这里,当前没有在执行的 定时器 并且 允许最后一次执行
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(() => {
// 这里给 old 重新赋值, 不然会因为上边的 now-old > wait 的误差 出现bug
old = Date.now()
timeout = null
func.apply(context, args)
}, wait)
}
}
// 取消方法
throttled.cancel = function () {
clearTimeout(timeout)
timeout = null
}
return throttled
}
let count = 0
let container = document.querySelector('#container')
let btn = document.querySelector('#btn')
function doSomeThing (e) {
console.log(e)
container.innerHTML = count++
console.log(this)
}
let doSome = throttle(doSomeThing, 1000)
btn.onclick = function () {
doSome.cancel()
}
// container.onmousemove = _.throttle(doSomeThing, 1000, {leading: true ,trailing: true})
// container.onmousemove = throttle(doSomeThing, 2000)
// container.onmousemove = throttle(doSomeThing, 2000, {leading: true, trailing: false})
// container.onmousemove = throttle(doSomeThing, 2000, {leading: false, trailing: true})
// container.onmousemove = throttle(doSomeThing, 2000, {leading: true, trailing: true})
container.onmousemove = doSome
由于节流函数实现步骤比较详细,这里的实现原理就不过多叙述了,从版本一 到版本四 一步步看下来理解会快一点。
节流使用场景:
- DOM 元素的拖拽功能实现
- 射击游戏
- 计算鼠标移动的距离
- 监听 scroll 滚动事件,规定时间内计算滚动高度等。