防抖节流优化版--最小修改代码无缝嵌入项目

683 阅读5分钟

前言

相信你经常在项目里面要接入一个防抖或者节流的需求。但是看着要把这个debounce方法接入到项目要改好多啊。对此我将debounce进行优化可以只修改一行代码直接无缝接入项目。

分为两部分介绍。

  1. 结合debounce和throttle,基础版
  2. 优化版debounce方法 首先来了解一下什么是防抖节流把。这边网上介绍的有很多。我找到一篇比较优质的。读者请先了解。

7分钟理解JS的节流、防抖及使用场景

第一步,结合debounce和throttle,基础版:

根据以上文章我们可以提炼出。debounce和throttle结合的基础代码:

/**
 * 防抖 节流
 * @param func 方法
 * @param wait 延迟时间
 * @param throttle 时间到多少了一定先执行, false 是防抖,true 节流
 * @returns {function(): void}
 */
const debounce = (func, wait = 500, throttle = false) => {
  let delay = wait || 500
  let timer
  return function () {
    const _this = this
    let args = arguments
    if (timer) {
      if (throttle) return
      else clearTimeout(timer)
    }
    timer = setTimeout(() => {
      timer = null
      func.apply(_this, args)
    }, delay)
  }
}
console.log('now time', new Date().toLocaleTimeString())
function testFunction (inString) {
  console.log(inString)
  console.log('now', new Date().toLocaleTimeString())
}
let debounceFunction = debounce(testFunction, 2000)
debounceFunction('debounceFunction start')
setTimeout(() => debounceFunction('debounceFunction start'), 1000)

let throttleFunction = debounce(testFunction, 2000, true)
throttleFunction('throttleFunction start')
setTimeout(() => throttleFunction('throttleFunction start'), 1000)

输出
now time 17:46:18
throttleFunction start
now 17:46:20
debounceFunction start
now 17:46:21

但是这个结合到项目里面真的就很方便吗?

如果我想让testFunction方法加上debounce的功能,我得先根据testFunction结合debounce生成一个debounceFunction,在调用这个方法才可以。现在可能觉得没什么。但是要把这个方法加到项目里面就很有点问题了。看例子:

情景一,按钮加防抖:

<template>
  ·······
  <el-button @click="searchEvent">查询</el-button>
  ·······
</template>

<script>
export default {
  methods: {
    searchEvent () {
      // 查看参数是否对
      this.initData()
    },
    initData () {
        // ······todo something
    }
  }
}

这个需求应该很普遍吧。在按钮上做防抖。如果是你会怎么做。可以很快想到,在initData方法外面套一层debounce就能解决。

<template>
  ·······
  <el-button @click="searchEvent">查询</el-button>
  ·······
</template>

<script>
import {debounce} from "../utils"
export default {
  methods: {
    searchEvent () {
      // 查看参数是否对
      this.initData()
    },
    initData: debounce(() => {
      // ······todo something
    }, 500)
  }
}
</script>

乍一看好像很ok啊,可以用啊。但是如果我想更新数据的同时不延时呢?

情景二,初始化请求不防抖:

如果我想在初始化的时候也查询一次initData呢?

······
import {debounce} from "../utils"
export default {
  mounted () {
    this.initData()
  },
  methods: {
    searchEvent () {
      // 查看参数是否对
      this.initData()
    },
    initData: debounce(() => {
      // ······todo something
    }, 500)
  }
}
</script>

这个时候初始化难道还要再等0.5秒吗?初页面如果慢个0.5秒对用户的体验是差很多的。 所以一般原来会怎么做?应该很快能想到吧?再写一个方法,可以直接调用的。

······
import {debounce} from "../utils"
export default {
  mounted () {
    this.initDataRunNow()
  },
  methods: {
    searchEvent () {
      // 查看参数是否对
      this.initDataDebounce()
    },
    initDataDebounce: debounce(() => {
      this.initDataRunNow()
    }, 500),
    initDataRunNow () {
      // ······todo something
    }
  }
}
</script>

我相信看到这里头就不开心了。虽然能解决问题,但是我要改好多代码啊!

一个地方调整一个方法,那其他按钮都要加防抖呢?我咋整。同时可以看到这个地方debounce的时间是固定的500ms,如果其他需求是要不同情况下200ms,500ms,1000ms那岂不是得写很多方法?这嵌入防抖的代价也太大了吧。 基于以上需求我们将代码进行优化修改。

优化debounce方法

先上用例:

方法加上debounce功能

<template>
  ·······
  <el-button @click="searchEvent">查询</el-button>
  ·······
</template>

<script>
import {debounceRun} from "../utils"
export default {
  mounted () {
    this.initData()
  },
  methods: {
    searchEvent () {
      // 查看参数是否对
      debounceRun(this.initData)
    },
    initData () {
      // ······todo something
    }
  }
}
</script>

看起来是不是简单了许多,只要在需要debounce的地方调用一下debounceRun就可以自动实现防抖。

支持方法传参

看到这里有小伙伴说了。啊你这如果有参数怎么办呢?那加上参数就好了。

searchEvent () {
      // 查看参数是否对
      debounceRun(this.initData, [a, b])
    },
    initData (a, b) {
      // ······todo something
    }

支持设置延时

又有人看到了说,啊你这防抖时间都没有自定义设置,真垃圾。那当然得支持啦,加上时间参数。

searchEvent () {
      // 查看参数是否对
      debounceRun(this.initData, [a, b], {wait: 450})
    },
    initData (a, b) {
      // ······todo something
    }

现在我相信会有小伙伴心动了。那么我就上代码啦:

const debounceRun = (() => {
  // 储存方法的timer的map
  let timerMap = new Map()
  return (func, args = [], options = {}) => {
    let defaultOptions = {
      funcKey: null,
      wait: 500,
      throttle: false
    }
    options = Object.assign(defaultOptions, options)
    // 如果没有mapKey 那么直接使用func作为map的key
    let mapKey = options.funcKey || func
    // 先看看map里面是否有timer,有timer代表之前调用过
    let timer = timerMap.get(mapKey)
    if (timer) {
      if (options.throttle) return
      else clearTimeout(timer)
    }
    timer = setTimeout(() => {
      // 先把这个方法从map里面删掉
      timerMap.delete(mapKey)
      func.apply(this, args)
    }, options.wait)
    // 将方法的timer村进map, key是mapKey
    timerMap.set(mapKey, timer)
  }
})()

通过以上方法可以直接进行方法防抖,同时不需要生成中间方法。

其他用法:

节流(throttle)用法:

debounceRun(this.initData, args, {wait: 250, throttle: true})

通过funcKey改造原方法

如果initData方法已经在项目其他地方用了很多了可以直接调整initData,这样就不需要每个initData的地方都加上debounceRun。

initData () {
  debounceRun(() => {
    // ······todo something
  }, { wait: 500, funcKey: this.initData })
}

以上使用funcKey是必须的,因为箭头方法是每次生成一次,所以使用的时候map的key就是不相等的。所以得使用funcKey说明这两个方法是属于一个。funcKey也可以是字符串,也可以是其他的对象,只要是唯一的就行。

funcKey参数妙用

使用funcKey参数也可以实现见两个方法只调用一个的效果。如:

let funcA = () => {console.log('funcA')}
let funcB = () => {console.log('funcB')}
不使用funcKey
debounceRun(func1)
debounceRun(func2)
输出:
funcA
funcB

使用funcKey
debounceRun(func1, [], {funcKey: 'mix_func_a_b'})
debounceRun(func2, [], {funcKey: 'mix_func_a_b'})

输出:
funcB
成功实现只有funcB输出而funcA不输出了

结尾

那么debounce的plus版本就算完成了。可以最小代码修改加入防抖/节流效果。 如果你觉得对你有帮助的话给一下赞咯~

就是听说点赞会加薪、升职、变帅、变美欸~

作者的其他文章:

将公司旧webpack3升级到5,踩坑总结

webpack5性能优化篇-前端必须会系列-分析如何加快构建并将包体积变小