setInterval 和 hooks 撞在一起,翻車了~

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事情是這樣子的,週末加班趕項目,有個同步數據功能爲異步進程,需要寫個輪詢來獲取同步結果。這功能簡單啊,輪詢我熟啊!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個 setInterval 就可以解決問題。於是,我不假思索寫下的功能代碼,測試都懶得測直接部署移測。(這種行爲是愚蠢而不負責任的,千萬不要效仿~)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"功能代碼是使用 react hooks 寫的,setInterval 並沒有如我所願的實現輪詢的功能,然後我懷疑人生了???","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"問題分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於需求很急,於是我把代碼暫時改成了 Class 組件的形式,重新發了一版,問題便解決了~","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是事情不能這樣子過去,我得思考下,爲什麼 setInterval 和 hooks 一起使用就滑鐵盧了呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們手動實現一個計時器例子來說明下,hooks 裏使用 setInterval 和 clearInterval 失效的根本原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function Counter() {\n const [count, setCount] = useState(0);\n \n useEffect(() => {\n let id = setInterval(() => {\n setCount(count + 1);\n }, 1000);\n return () => clearInterval(id); \n });\n \n return
{count}
;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你覺得這個代碼有問題嗎?請思考幾分鐘,再接着往下看!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際上上面的代碼是有問題的,React 默認會在每次渲染時,都重新執行 useEffect。而調用了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"clearInterval","attrs":{}}],"attrs":{}},{"type":"text","text":" 後重新 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"setInterval","attrs":{}}],"attrs":{}},{"type":"text","text":" 的時候,計時會被重置。如果頻繁重新渲染,導致 useEffect 頻繁執行,計時器可能壓根就不會被觸發!定時器也就失效了。這也是我寫的輪詢沒有生效的原因!","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"解決問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用過 hooks 的朋友,一定知道 useEffect 有第二個參數,傳入一個依賴數組,可以在依賴數組發生變更時候再次重新執行 effect,而不是每次渲染都執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼如果我們傳入一個空數組 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"[]","attrs":{}}],"attrs":{}},{"type":"text","text":" 作爲依賴,這樣子組件在掛載時候執行,在組件銷燬時候清理,是不是就可以解決問題呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"function Counter() {\n let [count, setCount] = useState(0);\n\n useEffect(() => {\n let id = setInterval(() => {\n setCount(count + 1);\n }, 1000);\n return () => clearInterval(id);\n }, []);\n\n return
{count}
;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但實際上呢,計時器更新到 1 之後,就停止不動了。計時器還是失敗了,無法實現輪詢功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲何現象與預期不符呢?其實仔細觀察,你會發現,這是個閉包的坑!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"useEffect 使用的 count 是在第一次渲染的時候獲取的。","attrs":{}},{"type":"text","text":" 獲取的時候,它就是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"0","attrs":{}}],"attrs":{}},{"type":"text","text":"。由於一直沒有重新執行 effect,所以 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"setInterval","attrs":{}}],"attrs":{}},{"type":"text","text":" 在閉包中使用的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"count","attrs":{}}],"attrs":{}},{"type":"text","text":" 始終是從第一次渲染時來的,所以就有了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"count + 1","attrs":{}}],"attrs":{}},{"type":"text","text":" 始終是 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"1","attrs":{}}],"attrs":{}},{"type":"text","text":" 的現象。是不是恍然大悟!如果在 hooks 中想要獲取一個有記憶的 count,這時候就會想起使用 useRef 了,也該它登場了~","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"useRef,有記憶的 hooks","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過上面的兩次失敗,我們總結兩個我們發現的矛盾點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、useEffect 是沒有記憶的,每次執行,它會清理上一個 effect 並且設置新的 effect。新的 effect 獲取到了新的 props 和 state;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、setInterval 是不會忘記的,它會一直引用着舊的 props 和 state,除非把它換了。但是如果它被換掉了,就會重新設置時間了;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"麻蛋,這水火不容啊,還好我知道有個 hooks 是有記憶的,那就是 useRef。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果在 effect 重新執行時,我們不替換計時器,而是傳入一個有記憶的 savedCallback 變量,始終指向最新的計時器回調,是不是問題就解決了呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們的方案大概是這樣的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設置計時器 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"setInterval(fn, delay)","attrs":{}}],"attrs":{}},{"type":"text","text":",其中 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"fn","attrs":{}}],"attrs":{}},{"type":"text","text":" 調用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"savedCallback","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一次渲染,設置 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"savedCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"callback1","attrs":{}}],"attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二次渲染,設置 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"savedCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" 爲 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"callback2","attrs":{}}],"attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"......","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們試着使用 useRef 重寫一下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"function Counter() {\n let [count, setCount] = useState(0);\n const savedCallback = useRef();\n \n function callback() {\n // 可以讀取到最新的 state 和 props\n setCount(count + 1);\n }\n \n // 每次渲染,更新ref爲最新的回調\n useEffect(() => {\n savedCallback.current = callback;\n });\n\n useEffect(() => {\n let id = setInterval(() => {\n savedCallback.current();\n }, 1000);\n return () => clearInterval(id);\n }, []);\n\n return
{count}
;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一方面傳入了","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"[]","attrs":{}}],"attrs":{}},{"type":"text","text":",我們的 effect 不會重新執行,所以計時器不會被重置。另一方面,由於設置了 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"savedCallback","attrs":{}}],"attrs":{}},{"type":"text","text":" ref,我們可以獲取到最後一次渲染時設置的回調,然後在計時器觸發時調用。這下數據都有記憶了,問題被解決了,不過這也太麻煩了,可讀性很差!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們抽取一下邏輯,自定義一個hooks 叫 useInterval 來代替 setInterval 的使用,保持使用方式一致。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"useInterval","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然上面的代碼有點羅裏吧嗦的,但是 hooks 有個強大的能力就是可以將一些邏輯提取出來,重組抽象爲一個自定義hooks,以便邏輯的複用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我希望我們的代碼最後是下面這樣子的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"function Counter() {\n const [count, setCount] = useState(0);\n\n useInterval(() => {\n setCount(count + 1);\n }, 1000);\n\n return
{count}
;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是我們把邏輯提取自定義了一個hooks,爲了語義化更好,我們命名爲 useInterval","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"function useInterval(callback) {\n const savedCallback = useRef();\n\n useEffect(() => {\n savedCallback.current = callback;\n });\n\n useEffect(() => {\n function tick() {\n savedCallback.current();\n }\n\n let id = setInterval(tick, 1000);\n return () => clearInterval(id);\n }, []);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏延時值是寫死的,我們需要參數化,考慮到,如果 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"delay","attrs":{}}],"attrs":{}},{"type":"text","text":" 變更了,我們也是要重新啓動計時器的,所以要將delay 放在 useEffect 的依賴中。改造一下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"function useInterval(callback,delay) {\n const savedCallback = useRef();\n\n useEffect(() => {\n savedCallback.current = callback;\n });\n\n useEffect(() => {\n function tick() {\n savedCallback.current();\n }\n\n let id = setInterval(tick, delay);\n return () => clearInterval(id);\n }, [delay]);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好啦,現在我們不需要再關注這一堆羅裏吧嗦的邏輯了,在 hooks 中使用定時器,只需要使用 useInterval 代替 setInterval 即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是如果你想要暫停計時器呢?很簡單我們只需要改一下 delay 的邏輯,當 delay 爲 null 時,不設置計時器即可,我們再改造一下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"js"},"content":[{"type":"text","text":"// 最終版\nfunction useInterval(callback,delay) {\n const savedCallback = useRef();\n\n useEffect(() => {\n savedCallback.current = callback;\n });\n\n useEffect(() => {\n function tick() {\n savedCallback.current();\n }\n\n if (delay !== null) {\n let id = setInterval(tick, delay);\n return () => clearInterval(id);\n }\n }, [delay]);\n}\n\nfunction Counter() {\n const [count, setCount] = useState(0);\n const [delay, setDelay] = useState(1000);\n const [isRunning, setIsRunning] = useState(true);\n\n useInterval(() => {\n setCount(count + 1);\n }, isRunning ? delay : null);\n\n return
{count}
;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到現在,我們的 useInterval 可以處理各種可能的變更了:延時值改變、暫停和繼續,可比原來的 setInterval 強大很多了!","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hooks 和 Class 是兩種不同的編程模式,我們在使用 Hooks 時候可能會遇到一些奇怪的問題,但是不要慌,我們需要的是發現問題的根本原因,然後改變思維去解決它,而不是使用舊有思維。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,感謝您可以讀到這裏,我去改我的輪詢代碼去了,回見!","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章