这两个函数网上已经有不少实现了, 通常项目中直接用 lodash 或 underscore 的实现git
所以,写出一个完善的 debounce 和 throttle 不是本篇的目的github
理解这两个方法的实现思路,清楚使用场景才是重点chrome
防抖:你尽管触发(通知我要执行该函数),我执行算我输(误,,等你累了(离最后一次触发过了 wait 时间),我再执行微信
这里的 执行 表示函数的实际调用, 而 触发 仅仅是通知执行app
以坐电梯为例,电梯运行表示函数执行
,有人进电梯表示一次触发
:通知电梯运行。一段时间内没人进电梯,那么电梯就开始运行。函数
PS: 没人进电梯那么电梯也不会运行布局
仍是以坐电梯为例,咱们建立如下实体类测试
class Elevator {
/** * @param {number} no 电梯编号 */
constructor(no) {
this.no = no
}
run () {
console.log(`${this.no}号电梯开始运行`)
}
}
class People {
constructor(no) {
this.name = "员工" + no
}
into (elevator) {
console.log(`${this.name} 进入${elevator.no}号电梯`)
elevator.run()
}
}
复制代码
运行优化
let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)
// 员工0 进入0号电梯
// 0号电梯开始运行
new People(index++).into(elevator)
// 员工1 进入0号电梯
// 0号电梯开始运行
复制代码
有人进就立刻运行电梯,但现实 run 执行函数(电梯运行)成本是巨大的,员工1也不可能进入电梯。ui
所以咱们不能轻易的运行电梯。而且上面的实现,用户应该是不能直接让电梯运行的,只能通知电梯有人进电梯了。
咱们进行以下改造:
编写防抖函数
function debounce (func, wait) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(()=>{
console.log('防抖完毕..开始执行')
func()
}, wait);
}
}
复制代码
改造 Elevator 和 People
class Elevator {
/** * @param {number} no 电梯编号 */
constructor(no) {
this.no = no
// 对外提供的接口,用户告知电梯该运行了
this.notify = debounce(this._run, 3000)
}
// 伪装是私有方法,只能我本身调用
_run () {
console.log(`${this.no}号电梯开始运行`)
}
}
class People {
constructor(no) {
this.name = "员工" + no
}
into (elevator) {
console.log(`${this.name} 进入${elevator.no}号电梯`)
elevator.notify()
}
}
复制代码
刚刚的例子再从新运行一次.
let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)
new People(index++).into(elevator)
复制代码
不出意外,报了 Uncaught TypeError: Cannot read property 'no' of undefined
很明显,_run 方法执行的时候里面的 this 值为 undefined
缘由在于 setTimeout 中 func 的调用方为全局做用域,在严格模式 (class 中的代码处于严格模式)下函数的 this 为 undefined
解法有多种:
this.notify = debounce(this._run.bind(this), 3000)
这样至关于对外部使用者进行了要求:必须进行 bind ,其实不太好
func.call(this)
此处的 this 指向为 notify 的调用方
注意 setTimeout 用的是箭头函数,不然 setTimeout 内函数的 this 是 window
与此同时,若是对 elevator.notify
进行传参的话,func 调用时忽略掉了!
所以对 debounce 进行以下改造:
function debounce (func, wait) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(()=>{
console.log('防抖完毕..开始执行')
func.apply(this,arguments)
}, wait);
}
}
复制代码
其余代码调整了下输出:
class Elevator {
/** * @param {number} no 电梯编号 */
constructor(no) {
this.no = no
// 对外提供的接口,用户告知电梯该运行了
this.notify = debounce(this._run, 3000)
}
// 伪装是私有方法,只能我本身调用
_run (...args) {
console.log("最后一次调用传入的参数为:",args)
console.log(`${this.no}号电梯开始运行`)
}
}
class People {
constructor(no) {
this.name = "员工" + no
}
into (elevator) {
console.log(`${this.name} 进入${elevator.no}号电梯`)
elevator.notify(this.name)
}
}
复制代码
测试输出
let elevator = new Elevator(0)
let index = 0
new People(index++).into(elevator)
new People(index++).into(elevator)
setTimeout(() => {
new People(index++).into(elevator)
}, 1000);
new People(index++).into(elevator)
// 员工0 进入0号电梯
// 员工1 进入0号电梯
// 员工2 进入0号电梯
// ... 等待1s
// 员工3 进入0号电梯
// ... 等待3s
// 防抖完毕..开始执行
// 最后一次调用传入的参数为: ["员工3"]
// 0号电梯开始运行
复制代码
至此,咱们的实现就能达到基本需求了。 若是看了 underscore 等开源库的话,会发现它还实现了其余需求
经常使用场景:初次点击搜索框控件,进行一次查询
初次查询获取到完整列表
仍是以上电梯为例,这里为了方便理解,咱们所说的电梯执行,是快速把人送到又回来等待别人进入。
取消防抖:电梯运行前要等 wait 时间,这时候电梯有个功能,按了某个按钮后,不用等 wait 时间,只要有新的人进电梯电梯立马运行(leading=true),或者从新开始防抖处理(leading=false)
根据需求进行配置,能够看出来,咱们比较经常使用的是第一种,这也是 lodash 的默认设置
固然,上面这些需求的实现不是本文的重点,感兴趣的话能够直接看开源库源码和文章底部的拓展阅读,其实不会很难~
节流:顾名思义,用来减小函数的执行次数的,固定过一段时间后才会执行。
以和产品撕逼为例,作需求表示函数执行
,提需求表示一次触发
:通知你作需求。产品初次给你提了一个需求,但是你很忙(你以为有坑),你让TA理清了再来,过段时间你再作(你是有原则的,从第一次提需求开始固定时间后你必定去作),这段时间产品能够对需求进行变动优化 ~ 。 而后产品又给你提了一个需求……
PS: 若是产品没提需求,那天然也不用作了
试想一下,这里若是用防抖的场景会如何?
是否是就像产品时不时的给你改需求,你每次都得从新设计方案- -。直到好久没改需求了,你才开始处理需求。
仍是以作需求为例,咱们建立如下实体类
/** * 研发 */
class RD {
/** * @param {number} no 研发编号 */
constructor(no) {
this.name = `研发` + no
// 用于需求方通知开发处理需求
this.notify = this._processing
}
// 伪装是私有方法,只能研发本身调用
_processing (...args) {
console.log("需求文档:", args)
console.log(`${this.name}开始处理需求`)
}
}
/** * 产品经理 */
class PM {
constructor(no) {
this.name = "产品经理" + no
}
request (rd, requirement) {
console.log(`${this.name} 请求 ${rd.name} 实现 ${requirement}`)
rd.notify(requirement)
}
}
复制代码
在不进行节流的状况下,场景以下
let rd = new RD(0)
let pm = new PM(0)
pm.request(rd,"微信APP")
pm.request(rd,"抖音APP")
// 产品经理0 请求 研发0 实现 微信APP
// 需求文档: ["微信APP"]
// 研发0开始处理需求
// 产品经理0 请求 研发0 实现 抖音APP
// 需求文档: ["抖音APP"]
// 研发0开始处理需求
复制代码
研发估计得累死...
进行防抖的话呢?
// RD 中进行以下修改
this.notify = debounce(this._processing,5000)
function debounce (func, wait) {
let timer = null
return function () {
console.log('研发收到需求:',arguments)
clearTimeout(timer)
timer = setTimeout(()=>{
func.apply(this,arguments)
}, wait);
}
}
复制代码
效果以下:
let rd = new RD(0)
let pm = new PM(0)
pm.request(rd,"微信APP")
pm.request(rd,"抖音APP")
// 产品经理0 请求 研发0 实现 微信APP
// 研发收到需求: ["微信APP"]
// 产品经理0 请求 研发0 实现 抖音APP
// 研发收到需求: ["抖音APP"]
// 过了5s...
// 需求文档: ["抖音APP"]
// 研发0开始处理需求
复制代码
虽然还没开始处理,但不断被告知修改需求,也累的够呛
那换成节流呢?
// RD 中进行以下修改
// 表示初次接收到需求后,5s后开发必定会去作
this.notify = throttle(this._processing,5000)
function throttle (func, wait) {
let timer = null
return function () {
if (!timer) {
console.log('研发收到需求:', arguments)
timer = setTimeout(() => {
func.apply(this, arguments)
timer = null
}, wait);
}
}
}
复制代码
操做以下
let rd = new RD(0)
let pm = new PM(0)
pm.request(rd, "微信APP")
pm.request(rd, "抖音APP")
/*** 第0s ***/
// 产品经理0 请求 研发0 实现 微信APP
// 研发收到需求:["微信APP"]
// 产品经理0 请求 研发0 实现 抖音APP
/*** 第5s ***/
// 需求文档: ["微信APP"]
// 研发0开始处理需求
复制代码
好像有哪里不对?研发作的怎么是 微信APP
的需求,说明 func.apply(this, arguments)
传递的参数 arguments
不对
缘由在于箭头函数没有本身的 this 和 arguments ,因此该函数内这两个的值是拿的上层做用域 function 函数中的值,最关键的是,这个值是声明时肯定而不是执行时肯定的。
因为该箭头函数只在第一次 timer 为空的时候被声明,所以箭头函数里面的 arguments 的值就没有再改过了
咱们作个改造,将 arguments 提到上层做用域中
function throttle (func, wait) {
let timer = null
let args = []
return function () {
args = arguments
if (!timer) {
console.log('研发收到需求:', args)
timer = setTimeout(() => {
func.apply(this, args)
timer = null
}, wait);
}
}
}
复制代码
let rd = new RD(0)
let pm = new PM(0)
pm.request(rd, "微信APP")
pm.request(rd, "抖音APP")
setTimeout(()=>{
pm.request(rd, "今日头条APP")
},2000)
setTimeout(()=>{
pm.request(rd, "chrome app")
},6000)
/*** 第0s ***/
// 产品经理0 请求 研发0 实现 微信APP
// 研发收到需求:["微信APP"]
// 产品经理0 请求 研发0 实现 抖音APP
/*** 第2s ***/
// 产品经理0 请求 研发0 实现 今日头条APP
/*** 第5s ***/
// 需求文档: ["今日头条APP"]
// 研发0开始处理需求
/*** 第6s ***/
// 产品经理0 请求 研发0 实现 chrome app
// 研发收到需求: ["chrome app"]
/*** 第11s ***/
// 需求文档: ["chrome app"]
// 研发0开始处理需求
复制代码
至此,节流的基本功能就开发完成了。对比下开源实现,咱们还缺的功能有:
options.leading=true
: 咱们上面的实现就是 options.leading=false
的效果options.trailing=false
: 咱们上面的实现就是 options.trailing=true
的效果,即时间一到就会进行函数的执行。禁用后时间一到不会再执行一次以作需求为例,
因此,通常状况下咱们不能同时设置 leading
和 trailing
为 false。
返回结果 的意思就是:产品要求你作的初版需求立刻出效果
取消节流 的意思就是:产品告诉你领导你在偷懒,下次你立刻就收到产品的需求了,若是 leading=true ,那下一次需求立刻解决,不然仍是等待 wait 再作
相关的开源库源码能够参考:
接下来讲下二者的区别吧,其实能够用一句话归纳,最终什么时候执行取决于发起方仍是执行方
取决于发起方那么是 debounce ,取决于执行方那么是 throttle
仍是用作需求为例,debounce 的状况,研发偏向产品一段时间后不改需求才开始作需求,若是产品不断的改需求,那研发作需求的时间是不能控制的
而 throttle 的状况,研发偏向固定时间段后才作需求,这个时间段中,产品该不应需求都不影响我何时作需求
如下几个场景,使用哪一种策略更好,以及对应的配置项
这里就不给出答案了,欢迎评论~
这里是 github 上的最新原文