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執行重新創建。
