08月07, 2022

JS进阶(7)--高阶函数(2)--函数防抖与函数节流

骚操作:函数防抖与函数节流

JavaScript 中的函数大多数情况下都是由用户主动调用触发的,除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。

但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就有函数防抖函数节流

下面是函数被频繁调用的常见的几个场景:

  • mousemove 事件。如果要实现一个拖拽功能,需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置 DOM 的位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死。

  • window.onresize 事件。为 window 对象绑定了 resize 事件,当浏览器窗口大小被拖动而改变的时候,这个事件触发的频率非常之高。如果在 window.onresize 事件函数里有一些跟 DOM 节点相关的操作,而跟 DOM 节点相关的操作往往是非常消耗性能的,这时候浏览器可能就会吃不消而造成卡顿现象。

  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)

  • 搜索联想(keyup,keydown,input事件)

  • 监听滚动事件判断是否到页面底部自动加载更多(scroll事件)

比如一个搜索的场景(例如百度),当我在一个文本框中输入文字(键盘按下事件)时,需要将文字发送到服务器,并从服务器得到搜索结果,这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验,类似于下面的效果:

5be978627ac99

可是如何来实现上面的场景呢?如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的!想想看,我搜索“google”这样的单词,至少需要按 6 次按键,就这一个词,我需要向服务器请求 6 次,并让服务器去搜索 6 次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。

对于这些情况的解决方案就是函数防抖(debounce)或函数节流(throttle),其核心就是限制某一个方法的频繁触发。

函数防抖(debounce)

函数防抖,就是指触发事件后在规定时间内函数只能执行一次,如果在 规定时间内又触发了事件,则会重新计算函数执行时间。

简单的说,当一个动作连续触发,则只执行最后一次。 如,坐公交,司机需要等最后一个人进入才能关门。每次进入一个人,司机就会多等待几秒再关门。

函数节流(throttle)

限制一个函数在规定时间内只能执行一次。 如,乘坐地铁,过闸机时,每个人进入后3秒后门关闭,等待下一个人进入。

竖线的疏密代表事件执行的频繁程度。可以看到,正常情况下,竖线非常密集,函数执行的很频繁。而debounce(函数防抖)则很稀疏,只有当鼠标停止移动时才会执行一次。throttle(函数节流)分布的较为均已,每过一段时间就会执行一次。

常见应用场景 函数节流的应用场景 间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交 函数防抖的应用场景 连续的事件,只需触发一次回调的场景有:
  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

简单示例

在没有使用函数防抖之前

<body>
  <input type="text" id="username">
</body>
<script>
  let user = document.getElementById("username");
  user.oninput = function(e){
    console.log(e.target.value);
  }
</script>

每次已输入,就会出现下面的效果:

2022-07-26 16.53.13

封装函数防抖函数:

/**
 * 函数防抖
 * @param {function} func 一段时间后,要调用的函数
 * @param {number} wait 等待的时间,单位毫秒
 */
function debounce(func, delay) {
  // 设置变量,记录 setTimeout 得到的 id
  let timerId = null;
  return function (...args) {
    if (timerId) {
      // 如果有值,说明目前正在等待中,清除它
      clearTimeout(timerId);
    }
    // 重新开始计时
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

再次调用:

user.oninput = debounce((e) => {
  console.log(e.target.value);
}, 500);

效果: 2022-07-26 16.54.26

效果是立竿见影的

封装函数节流的函数:

const throttle = function (fn, delay) {
    let canRun = true,
        firstTime = true;
    return function (...args) {
        if (!canRun) return; // 注意,这里不能用timer来做标记,因为setTimeout会返回一个定时器id
        canRun = false;
        // 第一次直接执行
        if (firstTime) {
            fn.apply(this, args)
        }
        setTimeout(() => {
            if (firstTime) {
                firstTime = false;
            } else {
                    fn.apply(this, args)
            }
            canRun = true;
        }, delay)
    }
}

本文链接:http://www.yanhongzhi.com/post/js_ap_14.html

-- EOF --

Comments