使用 React Hooks 聲明 setInterval

來自Dan 寫的一篇文章:
https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/
建議讀原文

關(guān)鍵的一段代碼:

import React, { useState, useEffect, useRef } from 'react';

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // 保存新回調(diào)
  useEffect(() => {
    savedCallback.current = callback;
  });

  // 建立 interval
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

useInterval Hook 內(nèi)置了一個(gè) interval 并在 unmounting 的時(shí)候清除,它是一個(gè)作用在組件生命周期里的 setInterval 和 clearInterval 的組合。

以下為讀完后我的一點(diǎn)記錄,需要解決以下幾個(gè)問題:
回答這些問題之前,需要確定下, setInterval 的邏輯是應(yīng)該寫在 useEffect 中的,而不是寫在函數(shù)組件的第一層作用域內(nèi),這是因?yàn)??這是第一個(gè)問題

1.為什么最簡(jiǎn)單的寫法有問題
2.為什么useInterval 的寫法可以
寫在useEffect 后,可以確認(rèn)的是 setInterval 內(nèi)部使用的 state 和props 一直是舊的,不會(huì)更新,像是下面這段代碼:

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

出來的count 一直是1 ,不會(huì)再更新,這是因?yàn)樽畛蹁秩镜臅r(shí)候 count 是 0, setInterval 一直用的是 0 的 count .查看例子
修復(fù)它的一種方法是用像 setCount(c => c + 1) 這樣的 「updater」替換 setCount(count + 1),這樣可以讀到新 state 變量。但這個(gè)無法幫助你獲取到新的 props。

另一個(gè)方法是用 useReducer()。這種方法為你提供了更大的靈活性。在 reducer 中,你可以訪問到當(dāng)前 state 和新的 props。dispatch 方法本身永遠(yuǎn)不會(huì)改變,所以你可以從任何閉包中將數(shù)據(jù)放入其中。useReducer() 有個(gè)約束是你不可以用它執(zhí)行副作用。(但是,你可以返回新狀態(tài) —— 觸發(fā)一些 effect。)
setInterval 沒有及時(shí)地描述過程 —— 一旦設(shè)定了 interval,除了清除它,你無法對(duì)它做任何改變

3.setInterval 和 useInterval 的區(qū)別:
useInterval 中使用 refs 來解決這個(gè)問題的,首先要想想要解決什么問題,上面這個(gè)問題,想要的其實(shí)是能夠在 state 或者 props 變化的時(shí)候,interval 的回調(diào)也能夠看到,但是 interval 呢,聲明了它之后呢,是無法在不改變 delay 時(shí)間的情況下替換原來的 interval 的, 但是可以引入指向新 interval 回調(diào)的可變savedCallback,讓 interval 的回調(diào)變化。

  • 我們調(diào)用 setInterval(fn, delay),其中 fn 調(diào)用 savedCallback。
  • 第一次渲染后將 savedCallback 設(shè)為 callback1。
  • 下一次渲染后將 savedCallback 設(shè)為 callback2
    這個(gè)可變的 savedCallback 需要在重新渲染時(shí)「可持續(xù)(persist)」,所以不可以是一個(gè)常規(guī)變量,我們想要一個(gè)類似實(shí)例的字段。
    就是說 在一個(gè)生命周期中 savedCallback 這個(gè)變量是不變的,
    useRef 就像是可以在其 .current 屬性中保存一個(gè)可變值的“盒子”。

你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 對(duì)象以 <div ref={myRef} /> 形式傳入組件,則無論該節(jié)點(diǎn)如何改變,React 都會(huì)將 ref 對(duì)象的 .current 屬性設(shè)置為相應(yīng)的 DOM 節(jié)點(diǎn)。

然而,useRef()ref 屬性更有用。它可以很方便地保存任何可變值,其類似于在 class 中使用實(shí)例字段的方式。

這是因?yàn)樗鼊?chuàng)建的是一個(gè)普通 Javascript 對(duì)象。而 useRef() 和自建一個(gè) {current: ...} 對(duì)象的唯一區(qū)別是,useRef 會(huì)在每次渲染時(shí)返回同一個(gè) ref 對(duì)象。

請(qǐng)記住,當(dāng) ref 對(duì)象內(nèi)容發(fā)生變化時(shí),useRef不會(huì)通知你。變更 .current 屬性不會(huì)引發(fā)組件重新渲染。如果想要在 React 綁定或解綁 DOM 節(jié)點(diǎn)的 ref 時(shí)運(yùn)行某些代碼,則需要使用回調(diào) ref 來實(shí)現(xiàn)。

換一種表達(dá)方式 useRef 在 react hook 中的作用, 正如官網(wǎng)說的, 它像一個(gè)變量, 它就像一個(gè)盒子, 你可以存放任何東西. createRef 每次渲染都會(huì)返回一個(gè)新的引用,而 useRef 每次都會(huì)返回相同的引用。

這個(gè) createRef 又是何物?它和 useRef 又有什么區(qū)別呢?https://stackoverflow.com/questions/54620698/whats-the-difference-between-useref-and-createref

這個(gè) ref 對(duì)象類似一個(gè) class 的實(shí)例屬性,為什么這里需要一個(gè)實(shí)例屬性,這個(gè)實(shí)例屬性與 直接聲明一個(gè)變量的區(qū)別在哪里?(類似于this)
為的是 在組件的生命周期中 這個(gè)變量是不變的,變量的變化也不會(huì)引起組件的重新渲染。

下面描述是創(chuàng)建實(shí)例對(duì)象的時(shí)候
使用new命令時(shí),它后面的函數(shù)依次執(zhí)行下面的步驟。

1.創(chuàng)建一個(gè)空對(duì)象,作為將要返回的對(duì)象實(shí)例。
2.將這個(gè)空對(duì)象的原型,指向構(gòu)造函數(shù)的prototype屬性。
3.將這個(gè)空對(duì)象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
4.開始執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼。

實(shí)例對(duì)象與創(chuàng)建普通對(duì)象的區(qū)別?
實(shí)例對(duì)象有上面的 4個(gè)步驟, 一般的通過字面量聲明的對(duì)象 沒有

為什么使用 ref 來解決這個(gè)問題呢?
因?yàn)?ref 返回的對(duì)象在組件的生命周期內(nèi)是不變化的,這個(gè)對(duì)象的屬性的變化也不會(huì)引起組件的重新渲染,這很重要,是為了保存變化的回調(diào)函數(shù),并且在回調(diào)函數(shù)變化的時(shí)候,組件不要重新渲染。

要解決的這個(gè)問題的關(guān)鍵點(diǎn)在于:

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

實(shí)例在這里 https://codesandbox.io/s/jj0mk6y683?file=/src/index.js

現(xiàn)在的這種寫法,的執(zhí)行結(jié)果 會(huì)是 0, 1,1,1,1 并且 id 始終是一個(gè)
為什么會(huì)這樣呢?

// 第一次執(zhí)行
count = 0;
<h1>0</h1>
useEffect (() => {
// count 為 0
})

useEffect 中執(zhí)行的東西是 聲明一個(gè) interval 這個(gè)interval 在1 s之后 設(shè)置 count 為 1,于是時(shí)間很快滴 到 1s 了,當(dāng)前 count 為 0, 設(shè)置 count = 0 + 1, ok 完成了, 很開心吧,現(xiàn)在 count 變成 1 啦,這個(gè)interval 還要繼續(xù)執(zhí)行的,時(shí)間很快又 1s 了,因?yàn)?useEffect 只能執(zhí)行1 次呀, 它記住的只是它那次渲染的 count, interval 的回調(diào)中看到的 count 還是 0 呀, 它又要完成 將 count = 0 +1 的工作,于是 count 又變成了 1。

所以,這個(gè)問題的點(diǎn)在于,count 實(shí)際上已經(jīng)更新了,但是 useEffect 中的 interval 中的回調(diào)中是捕獲不到這個(gè)更新的,那就想著可以在每次有更新的時(shí)候,引用能看到更新的那個(gè)回調(diào)吧, 但是在回調(diào)發(fā)生變化的時(shí)候,這個(gè) interval id 它是不可以變的, 因?yàn)檫@樣也更合理,回調(diào)變化時(shí)為了拿到更新的值,但是 delay 時(shí)間如果變化,那就是另外一個(gè) interval 了,這兩還是不一樣的。

于是 自然地,因?yàn)?ref 返回的對(duì)象,可以由一個(gè)變化的 current, 讓這個(gè)curret 屬性指向新變化的可以拿到 更新過的 state/ props 的回調(diào)函數(shù),然后將 interval 的回調(diào)設(shè)置為 這個(gè) ref 對(duì)象的 current 屬性,由于 ref 對(duì)象的特殊性,他的current 屬性變化的時(shí)候,不會(huì)引起組件的重新渲染。完美地解決了這個(gè)問題。

function Counter() {
  const [count, setCount] = useState(0);
  const savedCallback = useRef();

  function callback() {
    setCount(count + 1);
  }

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

大神還提取個(gè)useHook

function useInterval(callback) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
}

簡(jiǎn)直完美,最后 delay 以參數(shù)形式表示,

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [delay]);
}

簡(jiǎn)直完美,在 delay 變化的時(shí)候,重置 interval, 本來就應(yīng)該這樣。

這個(gè)代碼:

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

補(bǔ)充下, useEffect hooks 函數(shù)組件的執(zhí)行過程
React: 給我狀態(tài)為 0時(shí)候的UI。
你的組件:

  • 給你需要渲染的內(nèi)容: <p>You clicked 0 times</p>。
  • 記得在渲染完了之后調(diào)用這個(gè)effect: () => { document.title = 'You clicked 0 times' }。
  • React: 沒問題。開始更新UI,喂瀏覽器,我要給DOM添加一些東西。
  • 瀏覽器: 酷,我已經(jīng)把它繪制到屏幕上了。
  • React: 好的, 我現(xiàn)在開始運(yùn)行給我的effect

運(yùn)行 () => { document.title = 'You clicked 0 times' }。

現(xiàn)在我們回顧一下我們點(diǎn)擊之后發(fā)生了什么:

  • 你的組件: 喂 React, 把我的狀態(tài)設(shè)置為1。

  • React: 給我狀態(tài)為 1時(shí)候的UI。
    你的組件:

  • 給你需要渲染的內(nèi)容: <p>You clicked 1 times</p>。

  • 記得在渲染完了之后調(diào)用這個(gè)effect: () => { document.title = 'You clicked 1 times' }。

  • React: 沒問題。開始更新UI,喂瀏覽器,我修改了DOM。

  • Browser: 酷,我已經(jīng)將更改繪制到屏幕上了。

  • React: 好的, 我現(xiàn)在開始運(yùn)行屬于這次渲染的effect

  • 運(yùn)行 () => { document.title = 'You clicked 1 times' }。

先根據(jù) initial state 渲染出 DOM 節(jié)點(diǎn), return 的部分
執(zhí)行 useEffect 的部分 這里面可能要 發(fā)生更新
更新 DOM 就是 return 的地方
useEffect 中的return 的地方 清理舊的
執(zhí)行新的 useEffect 

React 渲染{id: 20}的UI。
瀏覽器繪制。我們?cè)谄聊簧峡吹絳id: 20}的UI。
React 清除{id: 10}的effect。
React 運(yùn)行{id: 20}的effect。

4.Dan的寫法還可以帶來哪些新特性
暫停interval
加速interval
5.總結(jié)一下對(duì)自己寫代碼有什么啟發(fā)

react 的模型
react hooks 執(zhí)行過程是怎樣
useEffect 中通常執(zhí)行哪些代碼(請(qǐng)求數(shù)據(jù))
useEffect 中執(zhí)行 setInteval 清除 interval
編寫自定義 hooks

文章中一開始就提到了 React 編程模型,用聽得懂的話描述就是

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容