React hook中的经典问题,直接上代码
export default function App() {const [count, setCount] = useState(0)useEffect(() => {setInterval(() => {console.log(count);}, 1500)}, [])return (<div className="App">{count}<button onClick={() => setCount(count + 1)}>+1</button><h1>Hello CodeSandbox</h1><h2>Start editing to see some magic happen!</h2></div>);}
预想结果:
点击多次+1按钮,控制台能正确打印当前count的值
实际结果:
无论点击多少次+1按钮,控制台始终打印count值为0.
为什么会出现这种情况?
这其实涉及到了闭包的问题,之前有写过一篇关于闭包的文章:JavaScript填坑——闭包。可以前去复习下闭包的概念。在本例中,每一次的setCount都会使得App组件重新渲染,那么App组件函数也会重新执行。每一次重新执行带来的后果就是每次都会产生一个新的闭包。useEffect中的deps数组时空数组,也就意味着只有第一次创建时候才会执行里面的回调。那么里面的回调拿到的count值永远时App组件第一次创建的闭包中的count值,第一次创建的count值就是0.
如何避免闭包陷阱?
1.让useEffect读取到当前闭包的count值。
export default function App() {const [count, setCount] = useState(0)useEffect(() => {let timer = setInterval(() => {console.log(count);}, 1500)return () => clearInterval(timer)}, [count])return (<div className="App">{count}<button onClick={() => setCount(count + 1)}>+1</button><h1>Hello CodeSandbox</h1><h2>Start editing to see some magic happen!</h2></div>);}
将useEffect的deps数组加入变量count,意味着每次count变换重新渲染组件时,useEffect里的回调函数都会执行,读取当前闭包里的count值。同时return返回的回调清理上一次useEffect回调函数中的计时器。
2.使用useRef hook
export default function App() {const [count, setCount] = useState(0);const countRef = useRef(count);useEffect(() => {setInterval(() => {console.log(countRef.current);}, 1500);}, []);// 每次count更新,这个副作用回调同时更新countRef.current的值。useEffect(() => {countRef.current = count;}, [count]);return (<div className="App">{count}<button onClick={() => setCount(count + 1)}>+1</button><h1>Hello CodeSandbox</h1><h2>Start editing to see some magic happen!</h2></div>);}
useRef 每次 render 时都会返回同一个引用类型的对象,我们在每次count值更新时同时也更新countRef.current的值。那么,当定时器回调函数打印的countRef.current也将是最新的count值。
利用闭包在思考一遍:每次count更新App组件重新渲染,创建新的闭包。定时器创建时机时第一次App组件渲染,countRef也是第一次闭包中的变量。但countRef指向的是一个对象引用,该对象更改只会变更其值,不会变更其引用(useRef特性)。因此,即使打印回调一直读取第一次闭包中的变量,也能读取到最新的count值。
