【React】如何實現一個Interval Hookhtml
useInterval(() => {
// do something...
}, 1000);
複製代碼
可能你看過也寫過一些 react hook ,不過你對 hook 的種種行爲真的瞭解嗎?這篇文章爲你剖析 hook 對比 class component 「反常」 的那些事兒。前端
its arguments are 「dynamic」react
能夠注意到咱們的 setInterval 是接受一個 dealy 值的, 而且這個值是能夠由咱們的代碼控制的, 這意味着咱們能夠隨時調整這個值來作動態的改變.git
能夠作到這樣: 用一個 interval 控制另外一個 interval 的速度github
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.api
咱們一般會使用 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 就沒法實現了
咱們回到上個問題, 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>
];
}
複製代碼