美文网首页
自定义节流函数六步应对复杂需求

自定义节流函数六步应对复杂需求

作者: 一颗冰淇淋 | 来源:发表于2022-02-12 21:19 被阅读0次

    节流定义

    某些频繁操作的事件会影响性能,"节流"用来控制响应的时间间隔,当事件触发的时候,相对应的函数并不会立即触发,而是会按照特定的时间间隔,每当到了执行的响应间隔时,才会执行响应函数。

    节流案例

    网络游戏中的"飞机大战",键盘按键可以用于发射子弹,快速不停的敲击键盘,飞机不会不停的发射,而是以一定的时间间隔,来控制子弹与子弹的距离。比如系统设定一秒钟发射一次子弹,那么即使你在一秒钟内敲击了20次键盘,仍然只会发送1次子弹。

    节流使用场景

    在程序设计的过程中,很多场景都能够用到"节流"。

    • 输入框频繁输入、搜索
    • 按钮频繁点击、提交信息,触发事件
    • 监听浏览器的滚动事件
    • 监听浏览器的缩放事件

    没有使用节流时

    这里模拟一个商品搜索框,我们需要对用户输入的内容调用接口进行关联查询,来给用户进行搜索提示。
    当没有使用防抖时,我们会直接将函数绑定到对应的事件上。

    // html
    <input />
    
    // js代码
    const inputEl = document.querySelector("input");
    
    let count = 0;
    function inputEvent(event) {
        console.log(`${++count}次输入,获取的内容为:${event?.target?.value}`);
    }
    inputEl.oninput = inputEvent;
    

    input框内输入"javascriptcsses6",一共16个字符,所以方法调用了16次

    1_没有使用节流.png

    这样的方式性能很低,因为每输入一个字符就调用接口,对服务器造成的压力很大,"节流"的作用按指定时间执行函数,避免了多次执行造成的资源浪费。

    自定节流函数

    节流函数实现的原理是,按照指定的时间间隔来执行函数。

    第一步:基本版节流实现

    判断当前与上一次执行函数的时间间隔,如果超过了指定的时间间隔,就执行函数。

    function throttle(fn, interval) {
      // 将初始时间定为0
      let startTime = 0;
      
      const _throttle = function () {
        // 获取当前时间
        let currentTime = new Date().getTime();
        // 获取剩余时间(当前时间与指定间隔的距离)
        let restTime = interval - (currentTime - startTime);
        
        // 剩余时间小于等于0时,执行函数
        if (restTime <= 0) {
          // 执行传入的函数
          fn();
          // 将当前时间赋值给初始时间
          startTime = currentTime;
        }
      };
      return _throttle;
    }
    
    inputEl.oninput = throttle(inputEvent, 2000);
    

    这里指定的时间间隔为2秒钟,即2秒钟执行一次函数。

    2_使用了节流.png

    但此时我们发现,参数没有被传递过来,实际上this的指向也不对了

    第二步:拓展this和参数

    通过apply方法来改变this的指向,以及传递参数

    function throttle(fn, interval) {
      // 将初始时间定为0
      let startTime = 0;
      
      const _throttle = function (...args) {
        // 获取当前时间
        let currentTime = new Date().getTime();
        // 获取剩余时间(当前时间与指定间隔的距离)
        let restTime = interval - (currentTime - startTime);
        
        // 剩余时间小于等于0时,执行函数
        if (restTime <= 0) {
          // 通过apply改变this的指向和传递参数
          fn.apply(this, args);
          // 将当前时间赋值给初始时间
          startTime = currentTime;
        }
      };
      return _throttle;
    }
    

    此时this和参数都已经可以获取到了~

    3_this和参数.png

    到这里为止,已经实现了节流的大部分使用场景,下面的功能会更为复杂。

    第三步:函数立即执行

    在上面的函数定义中,输入第一个字符时,函数大概率会执行的,因为输入第一个字符的时间减去初始化的时间0秒钟,一般会大于设定的时间间隔。

    如果觉得在输入第一个字符时的函数执行没有必要,那么可以自定义参数,来控制函数是否会立即执行。

    参数 leading 控制函数立即执行,默认为true。

    function throttle(fn, interval, options = {}) {
      let startTime = 0;
      // leading默认值设置为true
      const { leading = true } = options ;
      
      const _throttle = function (...args) {
        let currentTime = new Date().getTime();
        
        // 不需要立即执行时,将初始值为0的startTime修改为当前时间
        if (!leading && !startTime) {
          startTime = currentTime;
        }
        let restTime = interval - (currentTime - startTime);
        
        if (restTime <= 0) {
          fn.apply(this, args);
          startTime = currentTime;
        }
      };
      return _throttle;
    }
    
    // 传入leading参数
     inputEl.oninput = throttle(inputEvent, 2000, {
        leading: false,
     });
    

    这样就会等待2s才会执行第一次函数调用

    4_leading参数.png
    第四步:函数最后一次执行

    "节流"只和函数的间隔时间有关,和最后一个字符输入完成无关。

    所以最后一个字符输入完成后,与上一次函数调用的时间间隔如果没有到指定的时间间隔时,此时函数是不会执行的。如果需要执行,需要自定义参数来控制函数执行。

    通过参数 trailing 控制函数最后一次执行,默认为false。当函数需要在最后执行时,在每个时间间隔还没有到执行时设置计时器,等到了时间间隔执行函数时,清空计时器,如果最后一个字符输入后还没有到指定间隔,则执行计时器中的内容。

    function throttle(fn, interval, options = {}) {
      let startTime = 0;
      // 设置一个计时器
      let timer = null;
      // leading默认值设置为true,trailing默认值设置为false
      const { leading = true, trailing = false } = options;
      
      const _throttle = function (...args) {
        let currentTime = new Date().getTime();
        
        // 不需要立即执行时,将初始值为0的startTime修改为当前时间
        if (!leading && !startTime) {
          startTime = currentTime;
        }
        let restTime = interval - (currentTime - startTime);
        
        if (restTime <= 0) {
          // 当存在计时器时,清空计时器
          if (timer) {
            clearTimeout(timer);
            timer = null;
          }
          fn.apply(this, args);
          startTime = currentTime;
          // 执行完成就不再执行下面计时器的代码,避免重复执行
          return;
        }
        
        // 如果需要最后一次执行
        if (trailing && !timer) {
          // 设置计时器
          timer = setTimeout(() => {
            timer = null;
            fn.apply(this, args);
            // 当需要立即执行时,开始时间赋值为当前时间,反之,赋值为0
            startTime = !leading ? 0 : new Date().getTime();
          }, restTime);
        }
      };
      return _throttle;
    }
    
    // 传入leading、trailing参数
    inputEl.oninput = throttle(inputEvent, 2000, {
        leading: false,
        trailing: true,
    });
    

    此时输入完最后一个字符,等待计时器中设置的时间间隔(restTime),函数会再次执行。

    5_trailing.png
    第五步:取消功能

    可能存在这样的场景,当用户搜索时点击了取消,或者关闭页面,此时就不需要再发送请求了。
    我们增加一个取消按钮,点击后终止操作。

    // html
    <input />
    <button>取消</button>
    
    // javascript
    function throttle(fn, interval, options = {}) {
      let startTime = 0;
      // 设置一个计时器
      let timer = null;
      // leading默认值设置为true,trailing默认值设置为false
      const { leading = true, trailing = false } = options;
      
      const _throttle = function (...args) {
        let currentTime = new Date().getTime();
        // 不需要立即执行时,将初始值为0的startTime修改为当前时间
        if (!leading && !startTime) {
          startTime = currentTime;
        }
        let restTime = interval - (currentTime - startTime);
        
        if (restTime <= 0) {
          // 当存在计时器时,清空计时器
          if (timer) {
            clearTimeout(timer);
            timer = null;
          }
          fn.apply(this, args);
          startTime = currentTime;
          // 执行完成就不再执行下面计时器的代码,避免重复执行
          return;
        }
        
        // 如果需要最后一次执行
        if (trailing && !timer) {
          // 设置计时器
          timer = setTimeout(() => {
            timer = null;
            fn.apply(this, args);
            // 当需要立即执行时,开始时间赋值为当前时间,反之,赋值为0
            startTime = !leading ? 0 : new Date().getTime();
          }, restTime);
        }
      };
      
      // 在函数对象上定义一个取消的方法
      _throttle.cancel = function () {
        // 当存在计时器时,清空
        if (timer) {
          clearTimeout(timer);
          timer = null;
          // 重置开始时间
          startTime = 0;
        }
      };
      return _throttle;
    }
    
    // 获取dom元素
    const inputEl = document.querySelector("input");
    const cancelBtn = document.querySelector("button");
    
    const _throttle = throttle(inputEvent, 2000, {
        leading: false,
        trailing: true,
    });
    
    // 绑定事件
    inputEl.oninput = _throttle;
    cancelBtn.onclick = _throttle.cancel;
    

    当点击了取消之后,就不会再执行计时器的内容

    6_取消功能.png
    第六步:函数返回值

    上面的"节流"函数执行后都没有返回值,如果需要返回值的话,有两种形式。

    回调函数
    通过参数中传递回调函数的形式,来获取返回值。

    function throttle(fn, interval, options = {}) {
      let startTime = 0;
      // 设置一个计时器
      let timer = null;
      // leading默认值设置为true,trailing默认值设置为false,传入回调函数用于接收返回值
      const { leading = true, trailing = false, callbackFn } = options;
      const _throttle = function (...args) {
        let currentTime = new Date().getTime();
        // 不需要立即执行时,将初始值为0的startTime修改为当前时间
        if (!leading && !startTime) {
          startTime = currentTime;
        }
        let restTime = interval - (currentTime - startTime);
        if (restTime <= 0) {
          // 当存在计时器时,清空计时器
          if (timer) {
            clearTimeout(timer);
            timer = null;
          }
          // 获取执行函数的结果
          const result = fn.apply(this, args);
          // 执行传入的回调函数
          if (callbackFn) callbackFn(result);
          startTime = currentTime;
          // 执行完成就不再执行下面计时器的代码,避免重复执行
          return;
        }
        if (trailing && !timer) {
          timer = setTimeout(() => {
            timer = null;
            // 获取执行函数的结果
            const result = fn.apply(this, args);
            // 执行传入的回调函数
            if (callbackFn) callbackFn(result);
            // 当需要立即执行时,开始时间赋值为当前时间,反之,赋值为0
            startTime = !leading ? 0 : new Date().getTime();
          }, restTime);
        }
      };
      // 在函数对象上定义一个取消的方法
      _throttle.cancel = function () {
        if (timer) {
          // 当存在计时器时,清空
          clearTimeout(timer);
          timer = null;
          // 重置开始时间
          startTime = 0;
        }
      };
      return _throttle;
    }
    
    const inputEl = document.querySelector("input");
    const cancelBtn = document.querySelector("button");
    
    // 传入回调函数用于接收返回值
    const _throttle = throttle(inputEvent, 2000, {
        leading: false,
        trailing: true,
        callbackFn: (value) => {
         console.log("获取返回值", value);
        },
    });
    inputEl.oninput = _throttle;
    cancelBtn.onclick = _throttle.cancel;
    

    每执行一次响应函数,就会执行一次回调函数。

    7_获取返回值-回调函数.png

    promise
    通过返回promise的形式来获取返回值

    function throttle(fn, interval, options = {}) {
      let startTime = 0;
      // 设置一个计时器
      let timer = null;
      // leading默认值设置为true,trailing默认值设置为false
      const { leading = true, trailing = false } = options;
      const _throttle = function (...args) {
        // 通过promise来返回结果
        return new Promise((resolve, reject) => {
          let currentTime = new Date().getTime();
          // 不需要立即执行时,将初始值为0的startTime修改为当前时间
          if (!leading && !startTime) {
            startTime = currentTime;
          }
          let restTime = interval - (currentTime - startTime);
          if (restTime <= 0) {
            // 当存在计时器时,清空计时器
            if (timer) {
              clearTimeout(timer);
              timer = null;
            }
            // 获取执行函数的结果
            const result = fn.apply(this, args);
            // 通过resolve返回成功的响应
            resolve(result);
            startTime = currentTime;
            return;
          }
          if (trailing && !timer) {
            timer = setTimeout(() => {
              timer = null;
              // 获取执行函数的结果
              const result = fn.apply(this, args);
              // 通过resolve返回成功的响应
              resolve(result);
              // 当需要立即执行时,开始时间赋值为当前时间,反之,赋值为0
              startTime = !leading ? 0 : new Date().getTime();
            }, restTime);
          }
        });
      };
      // 在函数对象上定义一个取消的方法
      _throttle.cancel = function () {
        if (timer) {
          // 当存在计时器时,清空
          clearTimeout(timer);
          timer = null;
          // 重置开始时间
          startTime = 0;
        }
      };
      return _throttle;
    }
    
    // 获取dom元素
    const inputEl = document.querySelector("input");
    const cancelBtn = document.querySelector("button");
    
    const _throttle = throttle(inputEvent, 2000, {
        leading: false,
        trailing: true,
    });
    
    // apply用于将this指向input元素
    const promiseCallback = function (...args) {
        _throttle.apply(inputEl, args).then((res) => {
         console.log("promise回调", res);
        });
    };
    
    // 绑定事件
    inputEl.oninput = promiseCallback;
    cancelBtn.onclick = _throttle.cancel;
    

    promise调用then方法获取返回值

    8_获取返回值-promise.png

    在开发中使用节流函数优化项目的性能,可以按如上方式自定义,也可以使用第三方库。

    关于防抖函数,可以参考这一篇文章,自定义防抖函数五步应对复杂需求

    以上就是防抖函数相关内容,关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~

    相关文章

      网友评论

          本文标题:自定义节流函数六步应对复杂需求

          本文链接:https://www.haomeiwen.com/subject/bgjxlrtx.html