重新学防抖debounce和节流throttle,及react hook模式中防抖节流的实现方式和注意事项

我的未来我决定 提交于 2020-10-02 10:39:48

概念理解

防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

说起防抖大家肯定会想到节流,着两个就跟一对双胞胎一样,让大家经常傻傻搞不清楚
我们先来看一下节流的概念

节流就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率

我拿电梯关门举个例子吧:
防抖

你按了电梯关门按钮,电梯还有三秒要关闭,在你要关闭前的1.5s,按了一次开门按钮电梯将会重新将要关闭时间重置为3秒

节流

你按了电梯关门按钮,在电梯将要关闭的三秒内,你再怎么按电梯按钮都不会响应

常用情况

防抖

在前端开发中会遇到一些频繁的事件触发,比如:
mousedown、mousemove
keyup、keydown

节流

在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

防抖实现方式

第一版

function debounce(func, wait) {
  return function () {
    let timer
    clearTimeout(timer)
    timer = setTimeout(() => {
      func
    }, wait)
  }
}

这个是最简单的一版debounce的实现方式,但是缺点有许多,我们先来看第一个

一,this 指向问题
我们可知在setTimeout 中this 指向windows 详情可 MDN看详细说明

第二版(修改this问题)

function debounce(func, wait) {
  return function () {
    let timer
    const contentThis = this
    clearTImeout(timer)
    timer = setTimeout(() => {
      func.apply(contentThis)
    }, wait)
  }
}

第二版中,this指向是正常情况,但是如果func 有参数的话,就会导致参数的丢失,所以我们开启第三版

第三版(参数丢失问题)

function debounce(func, wait) {
  return function () {
    let timer
    const contentThis = this
    let arg = arguments
    clearTImeout(timer)
    timer = setTimeout(() => {
      func.apply(contentThis, arg)
    }, wait)
  }
}

第三版完后,大家会发现还有一个问题,这个debounce 并不能立即执行,下面我们再改一下代码

function debounce(func, wait, immediate) {
  return function () {
    let timer = null
    const _this = this
    let arg = arguments
    if (timer) clearTImeout(timer)
    if (immediate) {
      let runNow = !tiemr
      timer = setTimeout(function () {
        timer = null
      }, wait)
      if (runNow) {
        func.apply(_this, arg)
      }
    } else {
      timer = setTimeout(() => {
        func.apply(contentThis, arg)
      }, wait)
    }
  }
}

我们还需要注意一种情况,当func 函数有返回值时我们需要将返回值带上,但是会有一个问题,当setTimeOut this指向的问题,导致再延时器中返回的数据只能是undefined,所以我们只能再immediate上添加

第四版(返回值问题)

function debounce(func, wait, immediate) {
  return function () {
  	let result
    let timer = null
    const _this = this
    let arg = arguments
    if (timer) clearTImeout(timer)
    if (immediate) {
      let runNow = !tiemr
      timer = setTimeout(function () {
        timer = null
      }, wait)
      if (runNow) {
        result = func.apply(_this, arg)
      }
    } else {
      timer = setTimeout(() => {
        func.apply(contentThis, arg)
      }, wait)
    }
    return result
  }
}

第五版(添加防抖取消代码)

_.debounce = function (func, wait, immediate) {
    var timeout, result;

    var later = function (context, args) {
        timeout = null;
        if (args) result = func.apply(context, args);
    };

    var debounced = restArgs(function (args) {
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(later, wait);
            if (callNow) result = func.apply(this, args);
        } else {
            timeout = _.delay(later, wait, this, args);
        }

        return result;
    });

    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };

    return debounced;
};

节流的实现方式

节流的原理我们在上面已经说过了,每隔一段时间就只执行一次事件
我们先看第一种方式

第一版(时间戳版)

function throttle(func, wait) {
  let previous = 0
  return function () {
    let now = +new Date()
    let _this = this
    let arg = arguments
    if (now - previous > wait) {
      func.apply(_this, arg)
      previous = now
    }
  }
}

使用方式如下

content.onmousemove = throttle(count,1000);

第二版(定时器版)

function throttle(func, wait) {
  return function () {
    let timer = null
    let arg = arguments
    if (!timer) {
      timer = setTimeout(() => {
        timer = null
        func.apply(context, arg)
      }, wait)
    }
  }
}

现在我们分析一下,上面两版

第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

所以我们将这两版总结一下,实现一版,鼠标移入能立即执行,但是停止触发的时候还能再次执行一次,代码如下:

第三版

function throttle(func, wait) {
  let previous = 0
  let later = function () {
    previous = +new Date()
    tiemr = null
    func.apply(_this, arg)
  }
  let throttled = function () {
    let now = +new Date()
    let remaining = wait - (now - previous)
    let _this = this
    let arg = arguments
    let tiemr = null
    if (remaining <= 0 || remaining > wait) {
      if (tiemr) {
        clearTimeout(tiemr)
        timer = null
      }
      previous = now
      func.apply(_this, arg)
    } else if (!tiemr) {
      tiemr = setTimeout(later, remaining)
    }
  }
  return throttled
}

上面的代码还有许多局限,比如说,我想要实现,鼠标移入不执行,但是停止触发的时候再执行一次的效果,或者鼠标移入立即执行,但是停止触发的时候不再执行的效果。对于这种情况我们应该如何去做呢?

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

我们来优化一下代码

代码


function throttle(func, wait, options) {
  let previous = 0
  if (!options) {
    options = {}
  }

  let later = function () {
    previous = options.leading === false ? 0 : new Date().getTime()
    tiemr = null
    func.apply(_this, arg)
    if (!timeout) _this = args = null
  }

  let throttled = function () {
    let now = new Date().getTime()
    if (!previous && options.leading === false) previous = now
    let remaining = wait - (now - previous)
    let _this = this
    arg = arguments
    if (remaining <= 0 || remaining > wait) {
      if (tiemr) {
        clearTimeout(timer)
        timer = null
      }
      previous = now
      func.apply(_this, arg)
      if (!tiemr) _this = arg = null
    } else if (!tiemr && options.trailing !== false) {
      timer = setTimeout(later, remaining)
    }
  }
  return throttled
}

注意

我们要注意有这样一个问题:

那就是 leading:false 和 trailing: false 不能同时设置。

如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});

以上就完成了函数防抖和节流的实现

react hook 下的防抖和节流

最近我在react hook 中使用防抖和节流发现不起作用,最后发现是自己没有掌握react hook 的机制导致的
当组件组件重新渲染的时候,hooks 会重新执行一遍,这样debounce高阶函数里面的timer就不能起到缓存的作用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破坏了。
所以我们可以利用React组件的缓存机制重新实现一个react hook 版的防抖和节流

react hook 防抖

import * as React from 'react'

export function useDebounce(fn, delay, dep = []) {
  const { current } = React.useRef({ fn, timer: null })
  React.useEffect(
    function () {
      current.fn = fn
    },
    [fn]
  )

  return React.useCallback(function f(...args) {
    if (current.timer) {
      clearTimeout(current.timer)
    }
    current.timer = setTimeout(() => {
      current.fn.call(this, ...args)
    }, delay)
  }, dep)
}

react hook 节流

import * as React from 'react'

export function useThrottle(fn, delay, dep = []) {
  const { current } = React.useRef({ fn, timer: null })
  React.useEffect(
    function () {
      current.fn = fn
    },
    [fn]
  )

  return React.useCallback(function f(...args) {
    if (!current.timer) {
      current.timer = setTimeout(() => {
        delete current.timer
      }, delay)
      current.fn.call(this, ...args)
    }
  }, dep)
}

react 使用防抖和节流的注意点

我在实现防抖的时候还遇到遇到一个问题,我再这一起记录一下吧
当我在实现react中触发input 组件中的onChange 事件时去发送一个异步请求,但是发现一直获取不到value 值,代码如下

  const onSearchChange = (e) => {
    e.persist()
    e.preventDefault()
    e.stopPropagation()
    setSearchValue(e.currentTarget.value)
    useDebounce((e) => {
      setFilterValue(e.currentTarget.value)
    }, 500)
  }

就是setFilterValue 一直获取不到值,最后经过调查发现了原由,
首先,在 onSearchChange 中,事件触发,就能获取到 event 对象,其中主要就是 event.target 就是当前触发事件的 dom 对象,由于 useDebounce延迟执行,导致了 onSearchChange 函数已经执行完了,进入了 react-dom 中相关一系列操作(进行了一系列复杂的操作),下面给出最关键的 executeDispatchesAndRelease,executeDispatchesAndRelease 方法释放 event.target 的值

        /**
 * Dispatches an event and releases it back into the pool, unless persistent.
 *
 * @param {?object} event Synthetic event to be dispatched.
 * @private
 */
        var executeDispatchesAndRelease = function(event) {
            if (event) {
                executeDispatchesInOrder(event);
                if (!event.isPersistent()) {
                    event.constructor.release(event);
                }
            }
        };

由于 event 在 useDebounce中作为了参数,内存中没有清除,执行上面的方法 event.target = null; event 为引用类型,一处改变,所有用到的地方都会改变。导致了 useDebounce中 event 也发生了变化。
解决办法:

  const onSearchChange = (e) => {
    e.persist()
    e.preventDefault()
    e.stopPropagation()
    setSearchValue(e.currentTarget.value)
    handleSearchChange(e.currentTarget.value)
  }

  const handleSearchChange = useDebounce((value) => {
    setFilterValue(value)
  }, 500)

参考文档:
JavaScript专题之跟着 underscore 学节流
React hooks 怎样做防抖?

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!