最后一次区分 debounce 和 throttle

775 阅读3分钟

对于 debounce 和 throttle 这俩概念,我发现,如果非要翻译成中文的「防抖」和「节流」,我很容易就混淆了,咱也不懂什么工业节流,单从字面意思看,debounce 和 throttle 都可以用来控制函数的高频调用,节流节流,节制水流,听着都有控制高频调用的含义,那么直接从英文区别好了。

我这么区分的:bounce ,这个单词出现时,脑子里就浮现出一串波浪 ~~~,而且是有节奏有规律地上下波动~~~~那么 debounce ,首先要反抗的就是这种有规律,把有规律变成没有规律。

没有规律的话,当然要看用户怎么操作了,跟着用户的节奏摇摆,用户操作不停歇,方法不执行~~

另一方, throttle 肯定就是有规律有节奏的控制方案了,完美区分。

场景

对于 throttle ,常见的使用场景有:onscrollonresize 监听,除了中途有规律有节奏地控制方法调用,最后都需要再执行一次方法。

debounce 一个很普通的场景是,输入框搜索查询,监听 input 的 onchange 事件,输入内容改变时发送请求查询数据,如果每次输入改变都要发送一个请求,那对服务器来说压力很大。

对于中文搜索,我们首先可以用以下代码来优化一遍(react ):

compositionstart 和 compositionend

componentDidMount() {
    const { current } = this.ref
    if (current) {
      current.addEventListener('compositionstart', this.handleCompositionStart)
      current.addEventListener('compositionend', this.handleCompositionEnd)
    }
  }

  handleCompositionStart = () => {
    const { current } = this.ref
    current.composing = true
  }

  handleCompositionEnd = e => {
    const { current } = this.ref
    current.composing = false
    this.handleQueryChange(e)
  }

componentWillUnmount() {
    const { current } = this.ref
    if (current) {
      current.removeEventListener(
        'compositionstart',
        this.handleCompositionStart
      )
      current.removeEventListener('compositionend', this.handleCompositionEnd)
    }
  }

handleChange = e => {
    const { current } = this.ref
    // 中文输入还没有完成,阻止 handleChange 调用
    if (current.composing) {
      return
    }

	// 外部 handleChange 调用
    this.props.handleChange(e)
  }

render () {
    return (
        <input
          onChange={this.handleChange}
          placeholder="输入..."
          ref={this.ref}
        />
    )
}

debounce

实现 bounce:

  • 要求: 1、可选择是否需要立即执行;2、停止输入后 1s 依然无输入再执行方法;

  • 注意:1、this 指向;2、参数传递

     function debounce(fn, options = {
       threshhold: 1000,
       immediate: false
     }) {
         if(!fn instanceof Function) {
             throw new TypeError('Expected a function')
         }
     
         let timer = null
         let hasCalled = false
      
         return function () {
     	// 首次执行
           if (options.immediate && !hasCalled) {
             fn.apply(this, arguments)
             hasCalled = true
           } else {
             clearTimeout(timer)
             timer = setTimeout(() => {
     		// 记得加上 this 处理,以及传递 arguments
                 fn.apply(this, arguments)
             }, options.threshhold)
           }
         }
     }
    

效果: codepen.io/insekkei/pe…

throttle:

实现 throttle :

  • 要求: 1、可选择是否需要立即执行;2、每隔 1s 执行一次方法;

  • 注意:1、this 指向;2、参数传递

     function throttle(fn, options = {
       threshhold: 1000,
       immediate: false
     }) {
         if(!fn instanceof Function) {
             throw new TypeError('Expected a function')
         }
     
         let timer = null
         let hasCalled = false
      
         return function () {
           if (options.immediate && !hasCalled) {
             fn.apply(this, arguments)
             hasCalled = true
           } else {
             if(!timer) {
                 timer = setTimeout(() => {
                     fn.apply(this, arguments)
                     timer = null
                 }, options.threshhold)
             }
           }
         }
     }
    

不过,有没有一种可能,throttle 不需要最后一次调用呢?那应该怎么实现?

可以通过上次执行方法之后与这次要执行的时间差是否小与 threshhold 来判断是否是最后一次调用:

  • 要求: 1、可选择是否需要立即执行;2、默认每隔 1s 执行一次;3、可选择是否需要最后执行

  • 注意:1、this 指向;2、参数要传递;3、immediate 和 needEnded 不能同时 false (比如只 start 时输入了一个字符,同时设置了 false ,理解起来让人分裂...)

     function throttle(fn, options = {
       threshhold: 1000,
       immediate: true,
       needEnded: false
     }) {
         if(!fn instanceof Function) {
             throw new TypeError('Expected a function')
         }
     
         let timer = null
         let hasCalled = false
         let previousTime = 0
      
         return function () {
           if (options.immediate && !hasCalled) {
             fn.apply(this, arguments)
             hasCalled = true
           } else {
             if(!timer) {
                 if (options.needEnded) {
                   timer = setTimeout(() => {
                       fn.apply(this, arguments)
                       timer = null
                   }, options.threshhold)
                 } else {
                   if(Date.now() - previousTime >= options.threshhold) {
                     timer = setTimeout(() => {
                         fn.apply(this, arguments)
                         timer = null
                         previousTime = Date.now()
                     }, options.threshhold)
                   } else {
                     timer = null
                   }
                 }
             }
           }
         }
     }
    

效果: codepen.io/insekkei/pe…