debounce与throttle

背景

测试同学提了个 bug,在搜索框中疯狂输入文字,不一会儿接口就报错了。这个 bug 实际上就是由于每输入一个字符时,就去请求接口引起的:

const handleSearch = () => {
	// ...请求接口
}
<Input onChange={handleSearch} />

解决方法是通过 debounce 来控制输入的频率。

实际上 debouncethrottle 这两个概念经常是一起出现,在工作中也常常使用到。在早年接触前端的时候,项目里有 lodash,因此直接使用其提供的 debouncethrottle 。而在一些简单的项目里,往往不引入 lodash,这个时候就需要自己实现,于此做个记录。

debounce

debounce 也就是防抖,看看 lodash 中 debounce 的定义:

_.debounce(func, [wait=0], [options={}])

返回一个函数,该函数会距离上次调用后,延迟 wait 毫秒后调用func

在上面的 bug 中,对原搜索方法进行防抖处理后,仅当用户停止输入 300 毫秒后,才请求接口进行搜索:

// 停止输入 300 ms 后,进行搜索
_.debounce(handleSearch, 300)

####throttle

throttle 我们称之为截流,同样看看 loads 中 throttle 的定义:

_.throttle(func, [wait=0], [options={}])

返回一个函数,该函数最多每 wait 毫秒调用一次 func

如果对搜索方法进行截流,则无论用户输入多快,每 1 秒仅请求一次:

// 停止输入 300 ms 后,进行搜索
_.debounce(handleSearch, 1000)

手写一个 debounce

function debounce(func, wait = 0) {
  let timeout
  return function(...args) {
    if(timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => func.call(this, ...args), wait)
  }
}

由于 func 被当作是参数传递进来,注意上面的 func 需要绑定 this。并且在使用时如果有 this 指向,需要显式指明 this,如:

const o = {
  a: 1,
  b: function() {
      console.log(this.a);
  }
}
const c = debounce(o.b).bind(o);

对 debounce 进一步拓展,如果需要在调用时立刻执行一次,则可以:

function debounce(func, wait = 0, head) {
  let timeout;
  return function (...args) {
    const callNow = head && !timeout;
    timeout && clearTimeout(timeout);
    timeout = setTimeout(() => {
      if(!callNow) {
        func.call(this, ...args);
      }
      timeout = null
    }, wait);
    if (callNow) {
      func.call(this, ...args)
    };
  };
}

手写一个 throttle

function throttle(func, timeFrame = 0) {
  let lastTime = 0;
  return function () {
      const now = new Date();
      if (now - lastTime >= timeFrame) {
          func.call(this, ...args);
          lastTime = now;
      }
  };
}

what’s more: rxjs

截流和防抖很多时候是出现在事件中的,使用 rxjs 则可以使用 debounceTime 和 throttleTime:

import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
	
const inputEle = document.getElementById('input')
const inputs = fromEvent(inputEle, 'input');
const result = inputs.pipe(debounceTime(1000));
// 如果是节流可使用 throttleTime
// const result = inputs.pipe(throttleTime(1000));
result.subscribe(x => console.log(x));

rxjs + react

如果是 react 中的事件需要防抖,那么可以通过 Subject 来实现:

import React, { useEffect } from "react";
import { Subject } from "rxjs";
import "rxjs/add/operator/debounceTime";

const value$ = new Subject().debounceTime(300);

export default function App() {
  useEffect(() => {
    const subscription = value$.subscribe(console.log)
      return () => subscription.unsubscribe()
  }, [])

  const onChange = e => {
    value$.next(e.target.value);
  };
  return (
    <div>
      <input onChange={onChange} />
    </div>
  );
}