React hooks 怎樣作防抖?

防抖是前端業務經常使用的工具函數,也是前端面試的高頻問題。平時面試候選人,手寫防抖人人都會,可是稍作修改就有小夥伴進坑送命。本文介紹瞭如何在react hooks中實現防抖。javascript

背景

防抖(debounce)是前端常常用到的一個工具函數,也是我在面試中必問的一個問題。團隊內部推廣React hooks之後,我在面試中也加入了相關的題目。如何實現一個useDebounce這個看起來很基礎的問題,實際操做起來卻讓不少背代碼的小夥伴漏出馬腳。前端

問題的安排每每是這樣的:java

  1. 什麼是防抖、節流,分別解釋一下?
  2. 在白紙上手寫一個防抖or節流函數,本身任選(限時4分鐘)
  3. react hooks有了解嗎?上機實現一個useDebounce、useThrottle
  4. tyepscript有了解嗎?用ts再來寫一遍
  5. ……

圍繞一個主題不斷切換考察點,這樣一輪下來,輕鬆又流暢,同時能夠試探出不少信息。react

實際狀況是,不少候選人在第3題就卡住了,不得不說很惋惜。面試

場景還原

寫一個防抖函數

一個經典的防抖函數多是這樣的:緩存

function debounce(fn, ms) {
  let timer;
  return function(...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn(...args)
      timer = null;
    }, ms);
  }
}
複製代碼

改爲react hooks

先提供測試用例:函數

export default function() {
  const [counter, setCounter] = useState(0);

  const handleClick = useDebounce(function() {
    setCounter(counter + 1)
  }, 1000)

  return <div style={{ padding: 30 }}> <Button onClick={handleClick} >click</Button> <div>{counter}</div> </div>
}
複製代碼

不少小夥伴會想固然的就改爲這樣:工具

function useDebounce(fn, time) {
  return debounce(fn, time);
}
複製代碼

簡單、優雅,還複用了剛纔的代碼,測試一下,看起來並無什麼問題:測試

1-7292504.gif ui

可是這個代碼若是放上生產環境,你會被用戶錘死。

真的嗎?

換個用例來試一下:

export default function() {
  const [counter1, setCounter1] = useState(0);
  const [counter2, setCounter2] = useState(0);

  const handleClick = useDebounce(function() {
    console.count('click1')
    setCounter1(counter1 + 1)
  }, 500)

  useEffect(function() {
    const t = setInterval(() => {
      setCounter2(x => x + 1)
    }, 500);
    return clearInterval.bind(undefined, t)
  }, [])


  return <div style={{ padding: 30 }}> <Button onClick={function() { handleClick() }} >click</Button> <div>{counter1}</div> <div>{counter2}</div> </div>
}

複製代碼

2-7292504.gif

當引入一個自動累加counter2就開始出問題了。這時不少候選人就開始懵了,有的候選人會嘗試分析緣由。只有深入理解react hooks在重渲染時的工做原理才能快速定位到問題(事實上出錯沒關係,可以快速定位問題的小夥伴纔是咱們苦苦尋找的)。

有的候選人開啓胡亂調試大法,慌忙修改setCounter1:

const handleClick = useDebounce(function() {
    console.count('click1')
    setCounter1(x => x + 1)
  }, 500)
複製代碼

固然結果依然錯誤,並且暴漏了本身對react hooks特性不夠熟悉的問題……

有的候選人猜到是重渲染緩存的問題,因而寫成這樣:

function useDebounce(fn, delay) {
  return useCallback(debounce(fn, delay), [])
}
複製代碼

在配合setCounter1(x => x + 1)修改的狀況下,能夠獲得正確的結果。但並無正確解決問題。依然是錯誤的。有興趣的讀者能夠復現一下這個現象,思考一下爲何,歡迎留言討論。

問題出在哪裏?

咱們在useDebounce裏面加個log

function useDebounce(fn, time) {
  console.log('usedebounce')
  return debounce(fn, time);
}
複製代碼

3-7292504.gif

控制檯開始瘋狂的輸出log。看到這裏,不少讀者就明白了。若是是前面表現稍好的候選人,我能夠提示到此。

每次組件從新渲染,都會執行一遍全部的hooks,這樣debounce高階函數裏面的timer就不能起到緩存的做用(每次重渲染都被置空)。timer不可靠,debounce的核心就被破壞了。

如何調整?

修復這個問題能夠有不少辦法。好比利用React組件的緩存機制:

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

  return useCallback(function f(...args) {
    if (current.timer) {
      clearTimeout(current.timer);
    }
    current.timer = setTimeout(() => {
      current.fn.call(this, ...args);
    }, delay);
  }, dep)
}
複製代碼

就能夠實現一個可靠的useDebounce。

同理咱們直接給出useThrottle的代碼:

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

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

最後

使用react hooks能夠幫助咱們把一些經常使用的狀態邏輯沉澱下來。同時,react hooks引入生產項目的初期要格外留意寫法和原理的差別所帶來的隱患。否則就跟上面的候選人同樣大意失荊州……

分析一下這道題易錯的緣由:

  • 馬虎大意。debounce很簡單,react hooks也不難,萬萬沒想到結合起來就有坑
  • 心態崩壞。面試場景下,遇到沒有見過的問題,沒法冷靜分析。
  • 對react hooks理解不夠深入,踩坑很少
  • 對debounce也不是足夠熟悉,有背代碼的嫌疑

因爲太多人掛在這個問題上,我決定把它分享出來,但願能夠幫到你們。

相關文章
相關標籤/搜索