zhongxia245 / blog

这是一个Blog, 如果喜欢可以订阅,是Watch, 不是 Star 哈。。。
https://zhongxia245.github.io/blog/
160 stars 33 forks source link

【20190902】防抖和节流函数 #109

Open zhongxia245 opened 5 years ago

zhongxia245 commented 5 years ago

一、What?

防抖函数(debounce) :某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次

函数节流(throttle) :某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。

简单来说 防抖函数:一个是延迟多久触发 节流函数:一个是多久触发一次 引用:https://github.com/lessfish/underscore-analysis/issues/20#issuecomment-252409737

二、When?

防抖函数:

  1. autocomplete组件,文本框输入内容后,提示相关内容
  2. 每次 resize/scroll 触发统计事件
  3. 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

节流函数:

  1. 监听滚动事件判断是否到页面底部自动加载更多
  2. DOM 元素的拖拽功能实现(mousemove)
  3. 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  4. Canvas 模拟画板功能(mousemove)

三、How?

这两个函数,是闭包的应用,它会返回一个新的函数。

因此,使用方式如下

// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

四、常见问题

1. React 中,如果类似这样使用防抖函数,会获取input 的值,因为 e.target 为空

<input  type="text" onChange={debounce(this.onChange, 300)} />

// 解决方案
class mime extends Component {
  constructor(props) {
    super(props);
    this.state = {};

    this.onGetData = debounce(this.onGetData, 300);
  }

  onGetData = (val) => {
    console.log(`getData By :${val}`);
  };

  onChange = (e) => {
    this.onGetData(e.target.value);
  };

  render() {
    return (
      <div>
        <h1>防抖函数的在 react 中的应用</h1>
        <input
          type="text"
          onChange={this.onChange}
        />
      </div>
    );
  }
}

终、参考文章

1、 《【进阶 6-4 期】深入浅出防抖函数 debounce》 2、《在 react 组件中使用 debounce 函数》

zhongxia245 commented 5 years ago

简单的防抖,节流函数的实现,面试中,可能让你手写这两个方法,因此需要自己理解下原理,然后敲一两遍,加深记忆。(看了都懂,听了都会,上手全废)

/**
 * 【简单版】防抖函数
 * 使用 setTimeout 和 clearTimeout 来实现
 * @param {function} fn 需要防抖的函数
 * @param {number} wait 防抖的等待时间
 */
const debounce = (fn, wait = 100) => {
  // 通过闭包记录浏览器标识
  let timer = null

  // 通过防抖处理,会返回一个新的函数
  return function(...args) {
    // 已经设置过定时器,则清空上一次的定时器,重新开始计时
    if (timer) clearTimeout(timer)

    // 设定一个新的定时器,定时器结束后开始执行传入的函数
  timer =   setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}

/**
 * 增加立即执行的防抖函数
 * @param {function} fn 需要防抖的函数
 * @param {number} wait 防抖的等待时间
 * @param {bool} immediate 是否立即执行
 */
const debounce1 = (fn, wait = 100, immediate = false) => {
  let timer = null
  return function(...args) {
    if (timer) clearTimeout(timer)

    // 放开始立即执行一次, 定时器未空的时候,就是第一次。
    if (immediate && !timer) {
      fn.apply(this, args)
    }

 timer =   setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}

/**
 * 【简单版】函数节流
 * @param {function} fn 需要防抖的函数
 * @param {number} wait 节流的等待时间
 */
const throttle = (fn, wait = 100) => {
  // 通过闭包记录开始时间
  let start = 0
  return function(...args) {
    let now = Date.now()

    // 当前时间减去开始时间,大于间隔的时间,则执行函数
    if (now - start > wait) {
      start = now
      fn.apply(this, args)
    }
  }
}

/**
 * 函数节流
 * 触发时间如果小于间隔时间,则使用防抖来执行函数
 * @param {function} fn 需要防抖的函数
 * @param {number} wait 节流的等待时间
 */
const throttle1 = (fn, wait = 100) => {
  // 通过闭包记录开始时间
  let start = 0
  let timer = null
  return function(...args) {
    let now = Date.now()

    // 当前时间减去开始时间,大于间隔的时间,则执行函数
    if (now - start > wait) {
      start = now
      fn.apply(this, args)
    } else {
      // 解决用户最后一次操作距离上一次触发时间的时长打不到 wait 时长,为用户的最后一次操作进行兜底
      if (timer) clearTimeout(timer)

      timer = setTimeout(() => {
        start = now
        fn.apply(this, args)
      }, wait)
    }
  }
}