image
曾经面试时候被问到过这个, 年少的我一脸无知...
后来工作中遇到了一个场景: 输入名称的同时去服务器校验名称是否重复, 但发现之前的代码竟然都没做限制, 输入一次发一次请求. 简直忍不了, 就在项目的 utils 里加上了防抖函数.
正好做一个总结, 加深印象.
函数防抖和节流, 都是控制事件触发频率的方法. 应用场景有很多, 输入框持续输入, 将输入内容远程校验, 多次触发点击事件, onScroll 等等.
为了说明问题, 假设一个场景: 鼠标滑过一个 div, 触发 onmousemove 事件, 它内部的文字会显示当前鼠标的坐标.
- <style>
- #box {
- width: 1000px;
- height: 500px;
- background: #ccc;
- font-size: 40px;
- text-align: center;
- line-height: 500px;
- }
- </style>
- <div id="box"></div>
- <script>
- const box = document.getElementById("box")
- box.onmousemove = function (e) {
- box.innerhtml = `${e.clientX}, ${e.clientY}`
- }
- </script>
效果是这样的:
image
在上边的场景下, 我们不希望触发一次就执行一次, 这就要用到防抖或节流. 下面我们看一下它们能为我们做什么吧.
防抖
函数防抖, 这里的抖动就是执行的意思, 而一般的抖动都是持续的, 多次的. 假设函数持续多次执行,
我们希望让它冷静下来再执行. 也就是当持续触发事件的时候, 函数是完全不执行的, 等最后一次触发结束的
一段时间之后, 再去执行. 先看一下效果:
image
分解一下需求:
持续触发不执行
不触发的一段时间之后再执行
那么怎么实现上述的目标呢? 我们先看这一点: 在不触发的一段时间之后再执行, 那就需要个定时器呀, 定时器里面调用我们要执行的函数, 将 arguments 传入.
封装一个函数, 让持续触发的事件监听是我们封装的这个函数
- function debounce(func, delay) {
- return function() {
- setTimeout(() => {
- func.apply(this, arguments)
- }, delay)
- }
- }
第二点实现了, 再看第一点: 持续触发不执行. 我们先思考一下, 是什么让我们的函数执行了呢? 是上边的 setTimeout.OK, 那现在的问题就变成了
持续触发, 不能有 setTimeout. 这样直接在事件持续触发的时候, 清掉定时器就好了.
- function debounce(func, delay) {
- let timeout
- return function() {
- clearTimeout(timeout) // 如果持续触发, 那么就清除定时器, 定时器的回调就不会执行.
- timeout = setTimeout(() => {
- func.apply(this, arguments)
- }, delay)
- }
- }
用法:
- box.onmousemove = debounce(function (e) {
- box.innerHTML = `${e.clientX}, ${e.clientY}`
- }, 1000)
节流
节流的意思是让函数有节制地执行, 而不是毫无节制的触发一次就执行一次. 什么叫有节制呢? 就是在一段时间内, 只执行一次.
同样, 我们分解一下:
持续触发并不会执行多次
到一定时间再去执行
效果是这样的:
image
思考一下, 持续触发, 并不会执行, 但是到时间了就会执行. 抓取一个关键的点: 就是执行的时机.
要做到控制执行的时机, 我们可以通过一个开关, 与定时器 setTimeout 结合完成.
函数执行的前提条件是开关打开, 持续触发时, 持续关闭开关, 等到 setTimeout 到时间了, 再把开关打开, 函数就会执行了.
我们看一下代码怎么实现:
- function throttle(func, deley) {
- let run = true
- return function () {
- if (!run) {
- return // 如果开关关闭了, 那就直接不执行下边的代码
- }
- run = false // 持续触发的话, run 一直是 false, 就会停在上边的判断那里
- setTimeout(() => {
- func.apply(this, arguments)
- run = true // 定时器到时间之后, 会把开关打开, 我们的函数就会被执行
- }, deley)
- }
- }
调用的时候:
- box.onmousemove = throttle(function (e) {
- box.innerHTML = `${e.clientX}, ${e.clientY}`
- }, 1000)
这样, 就实现了节流, 节流还可以用时间间隔去控制, 就是记录上一次函数的执行时间, 与当前时间作比较, 如果当前时间与上次执行时间的时间差大于一个值, 就执行.
说明一下[节流时, 后面操作中应该是因为 run=false 所以才直接 return, 但是不是在 return 之前 let run=ture 直接覆盖掉之前的 false]
这里可以看一下 throttle 函数内部, 和函数调用的时候. 首先看函数内部, 分解一下结构:
- function throttle(func, deley) {
- return function () {
- // 执行 func
- }
- }
那调用时候呢? 也分解一下:
throttle(function () { // 目标函数内容 }, 1000)
这里 throttle 函数执行的结果是其内部 return 的 function 的调用. 也就是说鼠标经过的事件监听实际上是这个被 return 的 function, 不断持续触发的是它, 而 throttle 函数只是提供了一个作用域, 内部用闭包声明了一个 run 的开关变量, 由于闭包的存在, run 这个变量会一直存在不被销毁, 而 let run = true 只在这个闭包 (可以理解为作用域) 内只声明了一次, 但它不会被持续执行, 所以 return 的函数内部的判断不会被它覆盖掉. 根据打印结果可以看出, 事实确实是如此:
image
总结
防抖和节流巧妙地用了 setTimeout, 来控制函数执行的时机, 优点很明显, 可以节约性能, 不至于多次触发复杂的业务逻辑而造成页面卡顿.
来源: http://www.jianshu.com/p/4ec8d0f3eddc