本文翻译自社区medium ,作者Sandro Dolidze
Introduction React Hooks,不像Class Components那样,React Hooks提供了一种更low-level的优化和构建应用的方式,当然同时,这也会带来隐形的bug和资源泄露的问题。 这篇文章中,我列举了12个例子来说明这些常见的问题和怎么去修复他们
我在实习的这半个月接到过开发进度条的需求,用setInterVal+get来实现,由于对于Hooks的理解不深,使用也不甚熟练,写出了很多bug,正好看到这篇文章,分享出来,也是对知识的巩固
Case Study:Implementing Interval 先看一下这个例子,设置一个从0开始的计时器,然后每隔500ms加1,一共有三个button,分别是:开始||停止||清除
<!-- more -->
level0 Hello World的水平 1 2 3 4 5 6 7 8 9 10 11 export default function Level00 ( ) {console .log('renderLevel00' );const [count, setCount] = useState(0 ); return ( <div> count => {count} <button onClick={() => setCount(count + 1 )}>+</button> <button onClick={() => setCount(count - 1)}>-</ button> </div> ); }
很好理解,用一个useState来生成一个State,用户点击来控制count+和-
level1 setInterval(这就是我写的bug版本。。) 1 2 3 4 5 6 7 8 export default function Level01 ( ) {console .log('renderLevel01' );const [count, setCount] = useState(0 ); setInterval(() => { setCount(count + 1 ); }, 500 ); return <div > count => {count}</div > ; }
从代码逻辑上来看是每隔500ms给count增加1,但是呢这个代码有一个很严重的问题那就是会造成资源泄露,并且这种实现方式也是错误的。很容易就会造成浏览器页面崩溃,因为上面的函数会在页面每次重渲染的时候触发,组件与此同时也会在每次渲染触发的时候创建一个新的setInterval。
Mutations, subscriptions, timers, logging, and other side effects are not allowed inside the main body of a function component (referred to as React’s render phase). Doing so will lead to confusing bugs and inconsistencies in the UI.
处理这种副作用我们要用到useEffect
level2 useEffect 1 2 3 4 5 6 7 8 9 10 export default function Level02 ( ) { console .log('renderLevel02' ); const [count, setCount] = useState(0 ); useEffect(() => { setInterval(() => { setCount(count + 1 ); }, 500 ); }); return <div > Level 2: count => {count}</div > ; }
这次我们使用useEffect,大多数的的副作用都在useEffect中进行。但是上面的方法也会造成资源泄露。useEffect会在每次渲染完成后运行,所以新的setInterval会在每次count发生变化的时候被创建
level3 run only once 1 2 3 4 5 6 7 8 9 10 export default function Level03 ( ) {console .log('renderLevel03' );const [count, setCount] = useState(0 ); useEffect(() => { setInterval(() => { setCount(count + 1 ); }, 300 ); }, []); return <div > count => {count}</div > ; }
给useEffect加上参数[],这样useEffect只会被在组件mount后调用一次,但是这样的话,count只会从0-1 另外一个问题就是,useEffect没有清除定时器
level4 cleanup 1 2 3 4 5 6 useEffect(() => { const interval = setInterval(() => { setCount(count + 1 ); }, 300 ); return () => clearInterval(interval); }, []);
这种方法在return里清除了定时器,避免了资源泄露,但是存在的问题依然和之前的那一种方式一样的,count只会从0-1
level5 use Count
as dependency 1 2 3 4 5 6 useEffect(() => { const interval = setInterval(() => { setCount(count + 1 ); }, 500 ); return () => clearInterval(interval); }, [count]);
这次我们把count当做依赖传给useEffect的第二个参数,在这个例子中,useEffect会在mount后和每次count发生改变的时候调用,同时,return清除函数会在count发生变化的时候清除上一次的资源。
这次搞对了,没有bug,但是也有一点误区。setInterval
每隔500ms就会创建/销毁。每个setInterval
都只会被调用一次
level6 setTimeout 1 2 3 4 5 6 useEffect(() => { const timeout = setTimeout(() => { setCount(count + 1 ); }, 500 ); return () => clearTimeout(timeout); }, [count]);
和level5的一样
level7 functional updates for useState 1 2 3 4 5 6 useEffect(() => { const interval = setInterval(() => { setCount(c => c + 1 ); }, 500 ); return () => clearInterval(interval); }, []);
在之前的例子中,我们在每次count发生变化的时候运行useEffect,我们每次都需要获取到最新的count值。
但是useState提供了一个API可以让我们在不用获取到现在的值的情况下就可以更新值,我们可以直接给useState传递一个函数就可以做到。
现在就比较完美了,我们仅仅使用了一个setInterval
就做到了这一点,return后的clearInterval
清除函数也就会在组件销毁时运行
level8 local vairable(这就是我写的那一坨屎) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default function Level08 ( ) { console .log('renderLevel08' ); const [count, setCount] = useState(0 ); let interval = null ; const start = () => { interval = setInterval(() => { setCount(c => c + 1 ); }, 500 ); }; const stop = () => { clearInterval(interval); }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</ button> </div> ); }
我们添加了start和stop的按钮,但是这个程序是不正确的,stop按钮并不起作用。每次渲染的过程中就会创建出一个新的setInterval引用,所以,stop的引用就会变成null
level9 useRef 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default function Level09 ( ) { console .log('renderLevel09' ); const [count, setCount] = useState(0 ); const intervalRef = useRef(null ); const start = () => { intervalRef.current = setInterval(() => { setCount(c => c + 1 ); }, 500 ); }; const stop = () => { clearInterval(intervalRef.current); }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</ button> </div> ); }
useRef就像是go-to hooks一样,如果你需要一个可变的变量,你可以用useRef获取。和局部变量不同的是,React确保了useRef在每次渲染的过程中返回的是相同的引用
这下代码看起来没问题了,但是还有一个隐形的bug,如果start被多次重复调用后,setInterval也会被重复调用,造成内存泄露
level10 useCallback 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default function Level10 ( ) { console .log('renderLevel10' ); const [count, setCount] = useState(0 ); const intervalRef = useRef(null ); const start = () => { if (intervalRef.current !== null ) { return ; } intervalRef.current = setInterval(() => { setCount(c => c + 1 ); }, 500 ); }; const stop = () => { if (intervalRef.current === null ) { return ; } clearInterval(intervalRef.current); intervalRef.current = null ; }; return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</ button> </div> ); }
在Interval已经开始的时候,我们就不去触发了。当然cleanrInterval(null)也不会产生任何bug。这样也避免了资源泄露。
但是这种方法也有一个问题就是,可能会有性能问题。
memorization 是React性能优化的一个主要工具,React.memo
进行浅比较,如果引用是相同的话,就不会进行重渲染。但是呢,如果把stop和start都传递给一个memorized 组件的话,整个memorization 就会失效,因为每次return都会返回新的引用。
React Hooks: Memoization
level11 useCallback 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default function Level11 ( ) { console .log('renderLevel11' ); const [count, setCount] = useState(0 ); const intervalRef = useRef(null ); const start = useCallback(() => { if (intervalRef.current !== null ) { return ; } intervalRef.current = setInterval(() => { setCount(c => c + 1 ); }, 500 ); }, []); const stop = useCallback(() => { if (intervalRef.current === null ) { return ; } clearInterval(intervalRef.current); intervalRef.current = null ; }, []); return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</ button> </div> ); }
现在组件我们使用useCallback来包裹,现在每次我们返回的都是相同的引用。现在代码没有资源泄露,运行正确,同时也没有性能问题,但是代码复杂度增加了
level12 custom Hook 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function useCounter (initialValue, ms ) { const [count, setCount] = useState(initialValue); const intervalRef = useRef(null ); const start = useCallback(() => { if (intervalRef.current !== null ) { return ; } intervalRef.current = setInterval(() => { setCount(c => c + 1 ); }, ms); }, []); const stop = useCallback(() => { if (intervalRef.current === null ) { return ; } clearInterval(intervalRef.current); intervalRef.current = null ; }, []); const reset = useCallback(() => { setCount(0 ); }, []); return { count, start, stop, reset }; }
为了简化代码,我们把复杂的逻辑封装到useCounter中,同时向外暴露出{ count, start, stop, reset } API,然后我们就可以这么使用useCounter了
1 2 3 4 5 6 7 8 9 10 11 12 export default function Level12 ( ) { console .log('renderLevel12' ); const { count, start, stop, reset } = useCounter(0 , 500 ); return ( <div> count => {count} <button onClick={start}>start</button> <button onClick={stop}>stop</ button> <button onClick={reset}>reset</button> </ div> ); }
Summary 总的来说还是见的业务太少了,对React Hooks的使用场景不熟悉,还是要多写,多练多思考,才能真正融会贯通