JavaScript 防抖和节流


JS 防抖和节流

防抖和节流 anti-shake and throttling


防抖

防抖:是指在事件触发后的n秒内,该功能(事件)只能执行一次。如果在n秒内再次出发该事件,或者反复出发该事件,将阻止该事件。通常使用 setTimeout 重新计算函数执行时间。

常见的场景包括:

  • 文本框智能提示/自动补全(keyup):比如搜索软件的搜索框,输入“google”,会自动关联“google play”、“google play store”、“google chrome”等,如果用户输入太快,每按一个键都去后台搜索一次,则会浪费很多带宽资源。我们希望延迟一会儿,让用户把关键词输入完整之后再去搜索。
  • 浏览器窗口调整大小(resize)或鼠标移动(mousemove)等事件:前段会有调整浏览器窗口大小的时候触发某个事件,或者鼠标移动的时候触发某个事件,但这些事件会非常频繁的触发,会导致网页特别的卡,并且我们完全不需要如此频繁的处罚,我们希望延迟几秒后再触发。
  • 查询或提交按钮(click):阻止过于频繁的点击查询按钮。

实现方法:使用 setTimeout 延迟事件触发,期间有新的事件触发则重新刷新 delay time。并且需要使用闭包(closure)保存 timeoutID


测试没有防抖

用一个按钮来测试是最简单的,其他功能都会增加额外的代码。

测试下面这段代码,打开浏览器开发者工具(F12),查看输入,每点一次按钮都会在控制台输出两段文本。



假设这个按钮每点一次按钮都是一次 ajax 查询,那么就会造成不断的请求网络。


添加防抖函数

假设学校的选修课申请页面,在姓名这一栏没有输入完整的情况下,会自动模糊查询,用户在没有输入完整的情况下就可以匹配完整的结果。一个学生输入“Trump”之后,自动匹配“Donald John Trump”。

或者搜索引擎的搜索框,用户可能自己也不知道完整的关键字是什么,“智能提示”可以帮助到用户。

实现这些功能我们会给这个文本框注册keyup事件,但这个事件在用户每次按下按键都会触发,用户输入“Trump”就触发了5次,这样频繁的发起请求,会给数据库和网络增加很多压力,实际上用户完全不需要如此频繁的“自动补全”,甚至用户在连续输入的时候,频繁的“智能提示/自动补全”可能会影响到用户原本连贯的输入,如果我们可以延迟一段时间触发事件,只有在用户输入一段文本停顿一下的时候才触发,那就完美了。

所以我们需要实现的是每一次触发事件都延迟触发,在这期间再触发事件则重新计算延迟时间,直到用户停下输入,停止触发事件,不在刷新延迟时间,那在一段延迟之后,才会真正的触发事件。这就是防抖,这种情况非常适合“智能提示“和”自动补全”。

实现方式很简单,使用setTimeout来延迟运行,重复触发则清除上一个 timerout再新建一个,这样就实现了每一次都延时并且重复触发则不断重新计算timeout。



这是防抖函数最核心的代码,有了思路之后,就是这么简单。


回调函数传参 方法1

添加传参时要注意,debounce 返回的嵌套函数是用来绑定事件的,所以这个嵌套函数不能接受我们传递的实参,前台函数的实参只可能是事件对象。我们要传参,需要从 debounce 函数传入,然后再又 debounce 内部 callback 的时候将函数传入 callback。

所以,如果使用 function 函数嵌套,要注意我们要使用外层debounce 函数的作用域下的 arguments,而不是嵌套函数作用域下的 arguments,为此我们需要在外层函数将 arguments 保存下来,返回函数会通过闭包保存这个值。

不过 ES6 的剪头函数并没有自己的作用域,它直接使用了父级函数的作用域,这样在剪头函数里头,可以直接使用 arguments 对象来读取实参。




优化一下代码


后续的code都在 ES6的基础上改动。


回调函数传参 方法2

第一种方式并不好,有一种更好的解决方案是 debounce 函数不接受 callback function 的参数,而是当调用 debounce 函数的时候,通过 band 函数传递参数。




立即执行版本

前面的代码是延迟执行,特别适合“自动填充”和“智能提示”。

而现在希望事件被立即执行,但是执行之后一段时间内需要防抖处理,不能被重复点击。需要过一段时间之后才能继续触发事件。这种防抖比较适合“提交”和“查询”按钮的防抖。

立即执行版本最简单的实现方式就是,回调函数立即执行而不是延迟执行,然后使用一个状态变量来确定是否允许执行,之前 setTimeout 那套逻辑用来控制状态变量。





节流

节流:是指一段时间内只能触发一次。在一段时间内连续触发同一个事件,触发一次之后会阻止后面的事件再次被触发。

使用场景:比如查询按钮,查询可能需要2秒左右时间,避免用户在未出结果前多次点击查询按钮,限制用户2秒内只能查询一次。

实现方式:防抖是使用 setTimeout,但是每次重复触发防抖函数都会重新刷新 timeout。节流其实差不多,也可以使用 setTimeout,但每次触发防抖函数不刷新 timeout,而是阻止再次 callback,直到限制时间结束。