防抖是前端業務經常使用的工具函數,也是前端面試的高頻問題。平時面試候選人,手寫防抖人人都會,可是稍作修改就有小夥伴進坑送命。本文介紹瞭如何在react hooks中實現防抖。javascript
防抖(debounce)是前端常常用到的一個工具函數,也是我在面試中必問的一個問題。團隊內部推廣React hooks之後,我在面試中也加入了相關的題目。如何實現一個useDebounce
這個看起來很基礎的問題,實際操做起來卻讓不少背代碼的小夥伴漏出馬腳。前端
問題的安排每每是這樣的:java
圍繞一個主題不斷切換考察點,這樣一輪下來,輕鬆又流暢,同時能夠試探出不少信息。react
實際狀況是,不少候選人在第3題就卡住了,不得不說很惋惜。面試
一個經典的防抖函數多是這樣的:緩存
function debounce(fn, ms) {
let timer;
return function(...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args)
timer = null;
}, ms);
}
}
複製代碼
先提供測試用例:函數
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引入生產項目的初期要格外留意寫法和原理的差別所帶來的隱患。否則就跟上面的候選人同樣大意失荊州……
分析一下這道題易錯的緣由:
因爲太多人掛在這個問題上,我決定把它分享出來,但願能夠幫到你們。