【React】如何實現一個Interval Hook
useInterval(() => {
// do something...
}, 1000);
複製程式碼
可能你看過也寫過一些 react hook ,不過你對 hook 的種種行為真的瞭解嗎?這篇文章為你剖析 hook 對比 class component “反常” 的那些事兒。
有了自帶的 setInterval 為何還要再實現一個
its arguments are “dynamic”
可以注意到我們的 setInterval 是接受一個 dealy 值的, 並且這個值是可以由我們的程式碼控制的, 這意味著我們可以隨時調整這個值來做動態的改變.
可以做到這樣: 用一個 interval 控制另一個 interval 的速度
class component 實現
第一次嘗試
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
return <h1>{count}</h1>;
}
複製程式碼
我們一開始一般會寫出這樣的實現, useEffect 設定 interval, return cleanup. 然而這樣寫會有個奇怪的表現...
react 在預設在每次 render 之後會重新執行 effects, 這其實也是 react 所預期的, 因為這樣能避免 a whole class of bugs.
我們通常會使用 effect 來訂閱, 退訂一些 api, 但是在 setInterval 上使用的時候就會有問題, 因為執行 clearInterval
和 setInterval
是有時間差的, 當 react 渲染過於頻繁的時候, 就會出現 interval 壓根沒機會執行的情況!
我們以 100ms 的頻率去渲染 counter 元件,我們會發現 count 值一直沒有更新
第二次嘗試
在上一個階段中, 我們的問題是重複執行 effects 導致了 interval 被清理的太早.
我們知道 useEffect 可以傳入一個引數來決定是否重複執行 effects, 試一下
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
複製程式碼
好的, 現在我們 counter 更新到 1 就停止了
發生了什麼?!
這其實是個很常見的閉包問題, 也有了對應的 lint.
我們的 effects 現在只會執行一次, 所以 effects 每次捕獲的 count 值都是第一次 render 的 count 值(0), 所以 count + 1
一直是 1
有一種 fix 的方式是, 用 setState 的函式引數, setCount(count => count + 1)
, 這樣我們就可以讀取最新的 state, 但是這種方式不是萬能的, 比如不能讀取最新的 props, 那麼假如我們需要根據最新的 props 來 setState 就無法實現了
使用 Refs
我們回到上個問題, count 無法被正確讀取的原因是 count 的值一直引用的是第一次 render 的.
那如果我們在每次 render 的時候動態地改變 setInterval(fn, delay)
中 fn 函式, 使這個函式帶上最新的 props 和 state, 並且這個 fn 函式要能在多次 render 之間可持續(persist), 這樣 setInterval 執行的時候, 就可以實時的讀取這個函式拿到最新的值了
第一版實現:
function setInterval(callback) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const tick = () => savedCallback.current();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
}
複製程式碼
支援動態 delay
和 暫停
的最終版:
function setInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const tick = () => savedCallback.current();
if (delay !== undefined) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
複製程式碼
我們可以用這個 hook 做一些更加好玩的事 -- 用一個 interval 控制另一個 interval 的速度,就是一開始我們看到的那個動圖的樣子。
練習
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`Clicked ${count} times`);
}, 3000);
});
return [
<h1>{count}</h1>,
<button onClick={() => setCount(count + 1)}>
click me
</button>
];
}
複製程式碼
猜猜列印結果?
看看 class component 的表現如何?
class Counter extends React.Component {
state = {
count: 0
};
componentDidUpdate() {
setTimeout(() => {
console.log(`Clicked ${this.state.count} times`);
}, 3000);
}
render() {
const { count } = this.state;
return [
<h1>{count}</h1>,
<button onClick={() => this.setState({ count: count + 1 })}>
click me
</button>
];
}
}
複製程式碼
如何改造上面的 class component 讓它跟使用 hook 的元件一樣列印不同值?
hook 版本的怎麼改能變得跟之前的 class component 一樣列印相同值呢?
function Counter() {
const [count, setCount] = useState(0);
const saved = useRef(count);
useEffect(() => {
saved.current = count;
setTimeout(() => {
console.log(`Clicked ${saved.current} times`);
}, 3000);
});
return [
<h1>{count}</h1>,
<button onClick={() => setCount(count + 1)}>
click me
</button>
];
}
複製程式碼