防抖和節流及對應的React Hooks封裝
阿新 • • 發佈:2021-02-22
## Debounce
> debounce 原意`消除抖動`,對於事件觸發頻繁的場景,只有最後由程式控制的事件是有效的。
防抖函式,我們需要做的是在一件事觸發的時候設定一個定時器使事件延遲發生,在定時器期間事件再次觸發的話則清除重置定時器,直到定時器到時仍不被清除,事件才真正發生。
```js
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`的必要。
```jsx
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`回撥函式卻是在指定延遲之後才會觸發。
```jsx
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):
```js
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`版本的節流函式:
>
> ```js
> 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`儲存)
```js
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;
}
```
對應的在手勢滑動中的使用:
```js
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 (
);
}
```
這樣以來,手勢的`yvalue`值一直變化,但是因為使用的是`throttleValue`,引發的`useEffect`回撥函式已經符合規則被節流,每秒只能執行一次,停止變化一秒後最後執行一次。
## 對值還是對函式控制
上面的`Hooks`封裝其實對值進行控制的,第一個防抖的例子中,輸入的`text`跟隨輸入的內容不斷的更新`state`,但是因為`useEffect`是依賴的防抖之後的值,這個`useEffect`的執行是符合防抖之後的規則的。
可以將這個防抖規則提前嗎? 提前到更新`state`就是符合防抖規則的,也就是隻有指定延遲之後才能將新的`value`進行`setState`,當然是可行的。但是這裡搜尋框的例子並不好,對值變化之後發起的請求可以進行節流,但是因為搜尋框需要實時呈現輸入的內容,就需要實時的`text`值。
對手勢觸控,滑動進行節流的例子就比較好了,可以通過設定`duration`來控制頻率,給手勢值的`setState`降頻,每秒只能`setState`一次:
```js
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 (
);
}
//常規節流函式
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`執行重新