如何實現一個Interval Hook

【React】如何實現一個Interval Hookhtml

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

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

有了自帶的 setInterval 爲什麼還要再實現一個

its arguments are 「dynamic」react

能夠注意到咱們的 setInterval 是接受一個 dealy 值的, 而且這個值是能夠由咱們的代碼控制的, 這意味着咱們能夠隨時調整這個值來作動態的改變.git

能夠作到這樣: 用一個 interval 控制另外一個 interval 的速度github

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.api

咱們一般會使用 effect 來訂閱, 退訂一些 api, 可是在 setInterval 上使用的時候就會有問題, 由於執行 clearIntervalsetInterval 是有時間差的, 當 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>
  ];
}
複製代碼

FE One

專欄其餘文章

FE One
關注咱們的公衆號FE One,會不按期分享JS函數式編程、深刻Reaction、Rxjs、工程化、WebGL、中後臺構建等前端知識

參考: overreacted.io/making-seti…

相關文章
相關標籤/搜索