防抖和節流及對應的React Hooks封裝

xuweiblog發表於2021-02-22

Debounce

debounce 原意消除抖動,對於事件觸發頻繁的場景,只有最後由程式控制的事件是有效的。

防抖函式,我們需要做的是在一件事觸發的時候設定一個定時器使事件延遲發生,在定時器期間事件再次觸發的話則清除重置定時器,直到定時器到時仍不被清除,事件才真正發生。

const debounce = (fun, delay) => {
  let timer;
  return (...params) => {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fun(...params);
    }, delay);
  };
};

如果事件發生使一個變數頻繁變化,那麼使用debounce可以降低修改次數。通過傳入修改函式,獲得一個新的修改函式來使用。

如果是class元件,新函式可以掛載到元件this上,但是函式式元件區域性變數每次render都會建立,debounce失去作用,這時需要通過useRef來儲存成員函式(下文throttle通過useRef儲存函式),是不夠便捷的,就有了將debounce做成一個hook的必要。

function useDebounceHook(value, delay) {
  const [debounceValue, setDebounceValue] = useState(value);
  useEffect(() => {
    let timer = setTimeout(() => setDebounceValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  return debounceValue;
}

在函式式元件中,可以將目標變數通過useDebounceHook轉化一次,只有在滿足delay的延遲之後,才會觸發,在delay期間的觸發都會重置計時。

配合useEffect,在debounce value改變之後才會做出一些動作。下面的text這個state頻繁變化,但是依賴的是debounceText,所以引發的useEffect回撥函式卻是在指定延遲之後才會觸發。

const [text,setText]=useState('');
const debounceText = useDebounceHook(text, 2000);
useEffect(() => {
  // ...
  console.info("change", debounceText);
}, [debounceText]);

function onChange(evt){
  setText(evt.target.value)
}

上面一個搜尋框,輸入完成1秒(指定延遲)後才觸發搜尋請求,已經達到了防抖的目的。


Throttle

throttle 原意節流閥,對於事件頻繁觸發的場景,採用的另一種降頻策略,一個時間段內只能觸發一次。

節流函式相對於防抖函式用在事件觸發更為頻繁的場景上,滑動事件,滾動事件,動畫上。

看一下一個常規的節流函式 (ES6):

function throttleES6(fn, duration) {
  let flag = true;
  let funtimer;
  return function () {
    if (flag) {
      flag = false;
      setTimeout(() => {
        flag = true;
      }, duration);
      fn(...arguments);
      // fn.call(this, ...arguments);
      // fn.apply(this, arguments); // 執行時這裡的 this 為 App元件,函式在 App Component 中執行
    } else {
      clearTimeout(funtimer);
      funtimer = setTimeout(() => {
        fn.apply(this, arguments);
      }, duration);
    }
  };
}

(使用...arguments和 call 方法呼叫展開引數及apply 傳入argument的效果是一樣的)

擴充套件:在ES6之前,沒有箭頭函式,需要手動保留閉包函式中的this和引數再傳入定時器中的函式呼叫:

所以,常見的ES5版本的節流函式:

function throttleES5(fn, duration) {
  let flag = true;
  let funtimer;
  return function () {
    let context = this,
      args = arguments;
    if (flag) {
      flag = false;
      setTimeout(function () {
        flag = true;
      }, duration);
      fn.apply(context, args); // 暫存上一級函式的 this 和 arguments
    } else {
      clearTimeout(funtimer);
      funtimer = setTimeout(function () {
        fn.apply(context, args);
      }, duration);
    }
  };
}

如何將節流函式也做成一個自定義Hooks呢?上面的防抖的Hook其實是對一個變數進行防抖的,從一個不間斷頻繁變化的變數得到一個按照規則(停止變化delay時間後)才能變化的變數。我們對一個變數的變化進行節流控制,也就是從一個不間斷頻繁變化的變數指定duration期間只能變化一次(結束後也會變化)的變數

throttle對應的Hook實現:

(標誌能否呼叫值變化的函式的flag變數在常規函式中通過閉包環境來儲存,在Hook中通過useRef儲存)

function useThrottleValue(value, duration) {
  const [throttleValue, setThrottleValue] = useState(value);
  let Local = useRef({ flag: true }).current;
  useEffect(() => {
    let timer;
    if (Local.flag) {
      Local.flag = false;
      setThrottleValue(value);
      setTimeout(() => (Local.flag = true), duration);
    } else {
      timer = setTimeout(() => setThrottleValue(value), duration);
    }
    return () => clearTimeout(timer);
  }, [value, duration, Local]);
  return throttleValue;
}

對應的在手勢滑動中的使用:

export default function App() {
  const [yvalue, setYValue] = useState(0);

  const throttleValue = useThrottleValue(yvalue, 1000);

  useEffect(() => {
    console.info("change", throttleValue);
  }, [throttleValue]);

  function onMoving(event, tag) {
    const touchY = event.touches[0].pageY;
    setYValue(touchY);
  }
  return (
    <div
      onTouchMove={onMoving}
      style={{ width: 200, height: 200, backgroundColor: "#a00" }}
    />
  );
}

這樣以來,手勢的yvalue值一直變化,但是因為使用的是throttleValue,引發的useEffect回撥函式已經符合規則被節流,每秒只能執行一次,停止變化一秒後最後執行一次。

對值還是對函式控制

上面的Hooks封裝其實對值進行控制的,第一個防抖的例子中,輸入的text跟隨輸入的內容不斷的更新state,但是因為useEffect是依賴的防抖之後的值,這個useEffect的執行是符合防抖之後的規則的。

可以將這個防抖規則提前嗎? 提前到更新state就是符合防抖規則的,也就是隻有指定延遲之後才能將新的value進行setState,當然是可行的。但是這裡搜尋框的例子並不好,對值變化之後發起的請求可以進行節流,但是因為搜尋框需要實時呈現輸入的內容,就需要實時的text值。

對手勢觸控,滑動進行節流的例子就比較好了,可以通過設定duration來控制頻率,給手勢值的setState降頻,每秒只能setState一次:

export default function App() {
  const [yvalue, setYValue] = useState(0);
  const Local = useRef({ newMoving: throttleFun(setYValue, 1000) }).current;
  
  useEffect(() => {
    console.info("change", yvalue);
  }, [yvalue]);

  function onMoving(event, tag) {
    const touchY = event.touches[0].pageY;
    Local.newMoving(touchY);
  }
  return (
    <div
      onTouchMove={onMoving}
      style={{ width: 200, height: 200, backgroundColor: "#a00" }}
    />
  );
}

//常規節流函式
function throttleFun(fn, duration) {
  let flag = true;
  let funtimer;
  return function () {
    if (flag) {
      flag = false;
      setTimeout(() => (flag = true), duration);
      fn(...arguments);
    } else {
      clearTimeout(funtimer);
      funtimer = setTimeout(() => fn.apply(this, arguments), duration);
    }
  };
}

這裡就是對函式進行控制了,控制函式setYValue的頻率,將setYValue函式傳入節流函式,得到一個新函式,手勢事件中使用新函式,那麼setYValue的呼叫就符合了節流規則。如果這裡依然是對手勢值節流的話,其實會有很多的不必要的setYValue執行,這裡對setYValue函式進行節流控制顯然更好。

需要注意的是,得到的新函式需要通過useRef作為“例項變數”暫存,否則會因為函式元件每次render執行重新建立。

相關文章