前端防抖(debounce)和节流(throttle)

前言

  • 最近在react hooks项目开发中用到了防抖和节流,基于对防抖、节流实现原理的理解,结合业界优秀的hook@umijs/hooks进行了实现,在这里作一个简单的总结。
  • 防抖和节流都是防止某一段时间内函数频繁触发,可是这两兄弟之间的原理却不同。
  • 简答的说,防抖是某一段时间内只执行一次,而节流是间隔时间执行。

应用场景

防抖(debounce)

  • search搜索框联想搜索,用户在不断输入值时,用防抖来节约请求资源。
  • window触发浏览器resize的时候,用防抖来让其只触发一次。

节流(throttle)

  • 鼠标不断点击触发,mousedown单位时间内只触发一次。
  • 监听滚动事件,好比是否滑到底部自动加载更多,用throttle来判断。

防抖(debounce)

原生实现

function debounce(fun, delay) {
    return function (args) {
        let that = this
        let _args = args
        clearTimeout(fun.id)
        fun.id = setTimeout(function () {
            fun.call(that, _args)
        }, delay)
    }
}
复制代码

react hooks实现

首先,基于react useEffect实现一个useUpdateEffect,在防抖和节流hook的实现中都会用到:react

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;
复制代码

接着,基于react useCallback、useUpdateEffect实现函数防抖useDebounceFn:git

  • 参数:useDebounceFn接受三个参数:触发回调函数fn、依赖项deps、延迟时间wait;
  • 返回值:useDebounceFn返回一个run、cancel方法来执行和取消触发回调函数fn;
import { DependencyList, useCallback, useEffect, useRef } from 'react';
import useUpdateEffect from '../useUpdateEffect';

type noop = (...args: any[]) => any;

export interface ReturnValue<T extends any[]> {
  run: (...args: T) => void;
  cancel: () => void;
}

function useDebounceFn<T extends any[]>(fn: (...args: T) => any, wait: number): ReturnValue<T>;
function useDebounceFn<T extends any[]>(
  fn: (...args: T) => any,
  deps: DependencyList,
  wait: number,
): ReturnValue<T>;
function useDebounceFn<T extends any[]>(
  fn: (...args: T) => any,
  deps: DependencyList | number,
  wait?: number,
): ReturnValue<T> {
  const _deps: DependencyList = (Array.isArray(deps) ? deps : []) as DependencyList;
  const _wait: number = typeof deps === 'number' ? deps : wait || 0;
  const timer = useRef<any>();

  const fnRef = useRef<noop>(fn);
  fnRef.current = fn;

  const cancel = useCallback(() => {
    if (timer.current) {
      clearTimeout(timer.current);
    }
  }, []);

  const run = useCallback(
    (...args: any) => {
      cancel();
      timer.current = setTimeout(() => {
        fnRef.current(...args);
      }, _wait);
    },
    [_wait, cancel],
  );

  useUpdateEffect(() => {
    run();
    return cancel;
  }, [..._deps, run]);

  useEffect(() => cancel, []);

  return {
    run,
    cancel,
  };
}

export default useDebounceFn;
复制代码

如何使用useDebounceFn这个防抖hook,举个例子:github

效果:快速点击button会频繁调用 run,但只会在全部点击完成 500ms 后执行一次相关函数。浏览器

import React, { useState } from 'react';
import { Button } from 'antd';
import { useDebounceFn } from '../useDebounceFn';

export default () => {
  const [value, setValue] = useState(0);
  const { run } = useDebounceFn(() => {
    setValue(value + 1);
  }, 500);
  return (
    <div>
      <p
        style={{
          marginTop: 16,
        }}
      >
        {' '}
        Clicked count: {value}{' '}
      </p>
      <Button onClick={run}>Click fast!</Button>
    </div>
  );
};

复制代码

节流(throttle)

原生实现

function throttle(fun, delay) {
    let last, deferTimer
    return function (args) {
        let that = this
        let _args = arguments
        let now = +new Date()
        if (last && now < last + delay) {
            clearTimeout(deferTimer)
            deferTimer = setTimeout(function () {
                last = now
                fun.apply(that, _args)
            }, delay)
        }else {
            last = now
            fun.apply(that,_args)
        }
    }
}
复制代码

react hooks实现

import { DependencyList, useCallback, useEffect, useRef } from 'react';
import useUpdateEffect from '../useUpdateEffect';

type noop = (...args: any[]) => any;

export interface ReturnValue<T extends any[]> {
  run: (...args: T) => void;
  cancel: () => void;
}

function useThrottleFn<T extends any[]>(fn: (...args: T) => any, wait: number): ReturnValue<T>;
function useThrottleFn<T extends any[]>(
  fn: (...args: T) => any,
  deps: DependencyList,
  wait: number,
): ReturnValue<T>;
function useThrottleFn<T extends any[]>(
  fn: (...args: T) => any,
  deps: DependencyList | number,
  wait?: number,
): ReturnValue<T> {
  const _deps: DependencyList = (Array.isArray(deps) ? deps : []) as DependencyList;
  const _wait: number = typeof deps === 'number' ? deps : wait || 0;
  const timer = useRef<any>();

  const fnRef = useRef<noop>(fn);
  fnRef.current = fn;

  const currentArgs = useRef<any>([]);

  const cancel = useCallback(() => {
    if (timer.current) {
      clearTimeout(timer.current);
    }
    timer.current = undefined;
  }, []);

  const run = useCallback(
    (...args: any) => {
      currentArgs.current = args;
      if (!timer.current) {
        timer.current = setTimeout(() => {
          fnRef.current(...currentArgs.current);
          timer.current = undefined;
        }, _wait);
      }
    },
    [_wait, cancel],
  );

  useUpdateEffect(() => {
    run();
  }, [..._deps, run]);

  useEffect(() => cancel, []);

  return {
    run,
    cancel,
  };
}

export default useThrottleFn;
复制代码

如何使用useThrottleFn这个节流hook,举个例子:bash

效果:快速点击button会频繁调用 run,但只会每隔 500ms 执行一次相关函数。antd

import React, { useState } from 'react';
import { Button } from 'antd';
import { useThrottleFn } from '../useThrottleFn';

export default () => {
  const [value, setValue] = useState(0);
  const { run } = useThrottleFn(() => {
    setValue(value + 1);
  }, 500);
  return (
    <div>
      <p
        style={{
          marginTop: 16,
        }}
      >
        {' '}
        Clicked count: {value}{' '}
      </p>
      <Button onClick={run}>Click fast!</Button>
    </div>
  );
};
复制代码

总结

  • 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会从新读条。
  • 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
相关文章
相关标签/搜索