前端防抖(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遊戲的射速,就算一直按着鼠標射擊,也只會在規定射速內射出子彈。
相關文章
相關標籤/搜索