如何實現一個Interval Hook

FEOne發表於2019-04-10

【React】如何實現一個Interval Hook

useInterval(() => {
  // do something...
}, 1000);
複製程式碼

可能你看過也寫過一些 react hook ,不過你對 hook 的種種行為真的瞭解嗎?這篇文章為你剖析 hook 對比 class component “反常” 的那些事兒。

有了自帶的 setInterval 為何還要再實現一個

its arguments are “dynamic”

可以注意到我們的 setInterval 是接受一個 dealy 值的, 並且這個值是可以由我們的程式碼控制的, 這意味著我們可以隨時調整這個值來做動態的改變.

可以做到這樣: 用一個 interval 控制另一個 interval 的速度

如何實現一個Interval Hook

class component 實現

如何實現一個Interval Hook

第一次嘗試

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 上使用的時候就會有問題, 因為執行 clearIntervalsetInterval 是有時間差的, 當 react 渲染過於頻繁的時候, 就會出現 interval 壓根沒機會執行的情況!

如何實現一個Interval Hook

我們以 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>
  ];
}
複製程式碼

猜猜列印結果?

如何實現一個Interval Hook

看看 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>
    ];
  }
}
複製程式碼

如何實現一個Interval Hook

如何改造上面的 class component 讓它跟使用 hook 的元件一樣列印不同值?

如何實現一個Interval 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>
  ];
}
複製程式碼

FE One

專欄其他文章

如何實現一個Interval Hook

FE One
關注我們的公眾號FE One,會不定期分享JS函數語言程式設計、深入Reaction、Rxjs、工程化、WebGL、中後臺構建等前端知識

參考: overreacted.io/making-seti…

相關文章