javascript|javascript防抖和节流原理

防抖和节流的总结: 1. 防抖(debounce) 防抖是限制频率,多次出发一次执行。
2. 节流(throttle) 节流是限制次数,限制规定时间内执行次数。
这篇文章以 underscore.js 工具库中的 防抖和节流函数作为演示,并探究其源码。
underscore.js 中文文档
本篇文章只对其原理进行探究,使用方式不做太多概述,如果不知道怎么使用的同学,可自行百度。
**下边是 html 演示代码, 里边引入debounce.js throttle.js 两个文件 **

防抖 节流> #container { width: 100%; height: 400px; background: #999; text-align: center; line-height: 400px; color: #fff; }
取消
src="http://img.readke.com/220828/1FK260C-0.jpg">src="https://www.it610.com/article/debounce.js">src="https://www.it610.com/article/throttle.js">

效果如下
javascript|javascript防抖和节流原理
文章图片

通过对 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 方法接受三个参数,第一:要被限制防抖的函数,第二:时间间隔,第三:是否立即执行,
  1. 默认为 false 延迟执行,第一次触发不会执行,规定时间内连续触发也不会执行,停止触发以后在规定时间执行一次,
  2. true 就是onmousemove事件被触发以后立即执行,在间隔时间内连续触发不会执行,停止触发也不会再次执行,从新触发,还会立即执行。然后在开始计算时间间隔
要思考的问题:
  1. 防抖节流函数都是高阶函数,会返回一个函数
  2. 函数内部 this 指向问题,可以用 apply call bind 这些方法, 或者使用箭头函数
  3. event 对象的问题,涉及到 dom 事件的,有可能会使用 event 对象
  4. 函数有返回值的问题
防抖 (debounce.js) 【javascript|javascript防抖和节流原理】根据上边的使用方式执行原理,和 问题,来完成 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 事件
节流 (throttle.js) 节流方法,根据 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

由于节流函数实现步骤比较详细,这里的实现原理就不过多叙述了,从版本一 到版本四 一步步看下来理解会快一点。
节流使用场景:
  1. DOM 元素的拖拽功能实现
  2. 射击游戏
  3. 计算鼠标移动的距离
  4. 监听 scroll 滚动事件,规定时间内计算滚动高度等。

    推荐阅读