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":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章