前端需要掌握的20个手写功能—防抖节流

·  阅读 461

前言

该系列是笔者整理的前端需要掌握的手写功能集合,这些功能的手撕需要具备一定的前端基础功底,在面试中也会高频的出现。笔者会将每个手写功能单独呈现为一篇,尽可能整理的细致,同时也不会让文章篇幅太长,内容太过杂乱,该篇为前端开发中经常使用的工具函数防抖和节流函数

防抖函数

什么是防抖函数

防抖函数的功能是实现,被触发的函数可以延迟执行,同时在一定时间内多次触发会更新延迟时间,最终实现的效果是,在一定时间内多次触发被防抖的函数,最终该函数只会执行最后被触发的那一次,若以游戏做比喻,那么防抖可以理解为给技能增加施法前摇动作,若前摇被打断则需要重新施法

防抖函数的使用场景

在什么场景下,需要使用防抖函数呢?下面简单的列举几个基础的列子,思考这些使用场景特点,以便日后自己合理的运用防抖函数

  1. 输入验证或动态搜索

在input输入框中无论是将用户输入的内容进行验证,或者说是根据用户输入关键词,查询后进行内容推荐,我们一般都是需要监听用户的输入,如果用户输入每一个字符后,我们都验证一下或发送一下请求,那么将极大的消耗资源

  1. 下拉刷新

用户下拉时进行数据刷新操作,假设用户无意义频繁的快速下拉(想一想你有没有在网络不好,或者无聊抽风的时候频繁下拉刷新页面),如果每次都监听下拉动作,然后进行数据请求也是会极大的消耗资源

  1. 按钮提交

用户在进行按钮提交时,如果误触连点,多次提交表单,也会多消耗资源,如果是非幂等操作,配合处理不当还会出现其他意想不到的问题

  1. 监听页面滚动

有时候我们想要页面在滚动到某一位置时执行某项操作,如请求指定数据或是加载特殊进入动画,如果是直接监听页面滚动,那么在每次进行滚动时,页面将会持续的进行监听函数的回调,这也是有损性能的

以上就是笔者提供的一些防抖函数的基础使用场景,当然防抖不是这些场景下的唯一解,有些情况下需要多种手段配合处理,但是不能否认防抖函数在这些场景下使用起来简单高效

防抖的实现

接下来我们将一步步的从基础思路开始,最终实现一个防抖函数

业务场景:监听用户输入的关键词,以此查询相关推荐数据,然后进行结果词条展示,这是一个比较常见的场景,一般在列表搜索页面,根据用户输入的关键词,进行动态的结果词条展示

一:首先我们完成基础的功能,监听用户输入然后动态获取数据

  <input type="text" oninput="handleInput(this.value)">
  <script>
    //监听用户输入
    function handleInput(value) {
      mockRequestFn(value).then(res => {
        console.log(`关键词“${value}”请求到的数据为`, res.data)
      })
    }
    
    //模拟一个请求函数
    const mockRequestFn = function (value) {
      //这里我们直接使用Promise和setTimeout模拟了请求
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const result = {
            status: 'ok',
            code: 200,
            data: [{ name: 'someGoods', price: 110 }]
          }
          resolve(result)
        }, 500)
      })
    }
  </script>
复制代码

二: 防抖的第一个特点,能让被触发的函数延迟执行。延迟执行你一定想到了setTimeout,那么我们就使用setTimeout来完成这一步的操作

    function handleInput(value) {
      setTimeout(() => {
          mockRequestFn(value).then(res => {
            console.log(`关键词“${value}”请求到的数据为`, res.data)
          })
      }, 1000);//假设延迟1秒,我们时间故意放长点可以体验一下
    }
复制代码

三: 现在是延迟执行了,但是事件处理函数依然是被频繁的触发,现在就要处理多次触发时重置延迟时间的问题了,所谓重置延迟时间,其实就是在再次触发函数时,如果上一次的触发还没执行,取消上一次的执行,在这里每一次的函数触发执行都是用setTimeout完成的,那就可以使用清除定时器,来结束上一个函数

    let timer = null
    function handleInput(value) {
      clearTimeout(timer) // 在函数执行时清除上一个计时器,达到重置时间的效果
      timer = setTimeout(() => {
        mockRequestFn(value).then(res => {
          console.log(`关键词“${value}”请求到的数据为`, res.data)
        })
      }, 1000)
    }
复制代码

防抖的基本思路就是这样的,但是现在的实现方法是远远不够的,存在以下几个问题:

  1. 首先这样的书写方式,无法多次复用,其他地方需要使用,还需再写一遍,造成代码重复,我们需要将其封装为函数使用

  2. 为了实现该功能,直接在全局定义变量timer,然后在函数中使用定义的全局变量,这样函数执行结果既不可控(受到外界timer的影响),同时timer也会污染全局变量

  3. 有些情况下,防抖处理的函数是需要被立刻执行的,先执行处理函数再进行防抖操作,比如当防抖函数运用在表单提交操作上,用户在首次点击提交时,我们不能将请求延迟,否则就会极大的影响用户体验(真实的防抖时间不会像我们例子中这么长,但即使在防抖时间很短情况下,延迟了防抖的时间再去请求,也会影响用户体验,具体是立即执行还是延迟执行,这个需要根据具体场景选择),例子中我们需要的效果是,用户持续输入时,进行防抖不请求,一但用户停止输入(过了防抖时间无操作),立刻去请求数据,这就是属于延迟执行的操作。

  4. 如果被防抖处理的函数具有返回值,需要将其返回值返回出来

发现以上问题后我们来将防抖操作封装成函数,进一步处理一下

防抖函数

/**
 * 防抖函数
 * @param {Function} fn 需要防抖处理的函数
 * @param {Number} time 防抖时间
 * @param {Boolean} triggleNow 是否立即触发
 * @returns {*} fn执行结果 
 */
function debounce(fn, time, triggleNow) {
  let timer = null;
  return function () {
    let _self = this,
      args = arguments,
      result = null;
    if (timer) {
      clearTimeout(timer);
    }
    if (triggleNow) {
      /**
       * 1. 如果第一次进来timer为null,直接执行回调
       * 2. 然后给timer赋值,过了防抖时间将timer再次置null
       * 3. 如果在防抖时间内再次触发了函数,clearTimeout会取消上一个将timer置null的操作,timer就有值了
       * 4. 只有timer为null时才会执行函数fn
      */
      const exec = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, time);
      if (exec) {
        result = fn.apply(_self, args);
      }
    } else {
      timer = setTimeout(() => {
        result = fn.apply(_self, args);
      }, time);
    }
    return result;
  }
}
复制代码

节流函数

节流函数其实和防抖函数的一部分都是想通的,下面我们简单介绍一下

什么是节流函数

在我们介绍防抖函数的时候,曾今打过一个比喻,防抖函数就好像是技能前摇,那么节流函数就可以理解为技能冷却。因为节流函数主要针对的是,在函数被多次触发时,在固定时间内只会执行一次

再次注意下防抖和节流两者的区别:

防抖: 设置间隔时间,在间隔时间内多次触发函数, 刷新间隔时间,只执行最后触发那一次(如果是立刻触发的形式,只触发最开始那次)

节流: 设置间隔时间,在间隔时间内多次触发函数,触发函数只会执行一次,到下个间隔时间后,才会再次执行一次,以此类推

节流使用场景

虽然节流和防抖的功能不一样,但是有些场景下,两者其实都可以使用,关键取决于你想实现什么样的效果,就比如表单提交时用户连点问题,防抖节流都能在该场景下使用

下面举几个节流使用比较合适的场景

  1. 场景缩放

对页面(场景)缩放时,有些地方我们需要去监控浏览器resize,scroll等实现操作,如果一直监听,然后进行业务处理就会很消耗性能,这里就可以用节流实现性能优化

  1. 用户鼠标持续点击操作

这个场景很宽泛,只要是允许用户持续点击,但是又想只在一定时间内只触发一次处理函数的场景都可以使用

  1. 页面触底加载更多

页面触底后请求数据加载更多,但在请求回来列表再次渲染前,用户可能会多次触底发送请求,这时就可以使用节流

节流的实现

因为有防抖的实现思路,节流就不叙述那么详细了,节流的实现在于,用设置的节流时间、事件处理函数上次触发时间、当前事件处理函数触发时间进行计算比较,如果事件处理函数两次的触发间隔时间大于节流时间,就可以执行函数,否则继续等待

节流函数

节流函数在实现时,有一个点需要注意,就是是否需要继续执行最后一次触发事件,有些时候我们在进行节流操作的时候,如果在节流时间内再次触发事件处理函数,虽然函数在该时间段内不会再次执行,但是会记录到下一个节流周期执行,而有些时候我们又不希望执行这样的操作,因此针对这种情况需要进行参数判断。

/**
 * 节流函数
 * @param {Function} fn 需要节流处理的函数
 * @param {Number} delay 节流时间
 * @param {Boolean} needLast 是否执行最后一次触发
 * @returns fn执行结果
 */
function throttle (fn, delay, needLast) {
  let timer = null,
    beginTime = new Date().getTime();
  return function () {
    let _self = this,
      args = arguments,
      curTime = new Date().getTime(),
      result = null;
    clearTimeout(timer)
    //判断两次间隔如果大于设置时间执行函数
    if (curTime - beginTime >= delay) {
      result = fn.apply(_self, args);
      beginTime = curTime;
    } else {
      //如果设置需要执行最后一次触发,那么就利用setTimeout延迟后执行
      if (needLast) {
        timer = setTimeout(() => {
          result = fn.apply(_self, args);
        }, delay);
      }
    }
    return result
  }
}
复制代码

结语

防抖函数和节流函数,本身并不涉及任何设计模式相关知识,仅仅是技能型工具函数,适用于前端某些业务场景,虽然代码并不多实现起来也相对简单,但是这也需要前端开发者对JS的基础知识掌握的足够扎实,因为其中也涉及到了闭包、apply(this指向相关)、arguments、clearTimeout和指示器等JS语言基础知识,由思路的推演还涉及到了函数式编程范式、纯函数的使用相关知识,这其中任何一点都值得再深入的了解,以此扎实自己的前端基础,因此下一篇将整理JS中this指向及call、apply、bind的基础实现,整理好后会附在本文最后。
上一篇手写文章地址:前端需要掌握的20个手写功能—Event Emitter/发布订阅模式

分类:
前端