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

Debounce

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

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

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

若是事件發生使一個變量頻繁變化,那麼使用debounce能夠下降修改次數。經過傳入修改函數,得到一個新的修改函數來使用。app

若是是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回調函數倒是在指定延遲以後纔會觸發。this

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

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

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


Throttle

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

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

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

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執行從新建立。

相關文章
相關標籤/搜索