因 useEffect 、 useState 会创建闭包,在某些场景下会导致意外的行为,这些异常现象称为 react Hooks 的闭包陷阱。
useState 闭包陷阱
setCount 后无法取到 count 的最新值
import { useState } from "react"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount(count + 1); console.log("add函数内count的值为", count); }; console.log("add函数外count的值为", count); return ( <div> <span>{count}</span> <button onClick={add}> + </button> </div> ); }
点击按钮后执行结果为
add函数内count的值为 0 add函数外count的值为 1
原因解析
useState 修改响应式变量的值是异步执行的,即在 add函数 执行期间,count 并没有被改变!
待 useState 完成对响应式变量 count 的修改后,会触发组件重新渲染(组件函数会再次执行),因此时 count 已修改,所以会打印 add函数外count的值为 1
解决方案
不要在 setCount 后,拿 count 做其他操作!
多次 setCount 只会执行最后一次
import { useState } from "react"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount(count + 2); setCount(count + 1); }; console.log("count的值为", count); return ( <div> <span>{count}</span> <button onClick={add}> + </button> </div> ); }
点击按钮后执行结果为
count的值为 1
原因解析
在一个渲染周期内,对同一响应式变量进行多次修改时,仅最后一次修改有效!(为了提升渲染性能)
解决方案
setCount 改传入一个函数(实现每一次 count 都是取的修改后的新值!)
import { useState } from "react"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount((count) => count + 2); setCount((count) => count + 1); }; console.log("count的值为", count); return ( <div> <span>{count}</span> <button onClick={add}> + </button> </div> ); }
最佳实践
setCount 的参数,都用函数的形式传入!
useEffect 闭包陷阱
import { useState, useEffect } from "react"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount(count + 1); }; useEffect(() => { setInterval(() => { console.log(`setInterval中 count 的值为: ${count}`); }, 1000); }, []); console.log("count的值为", count); return ( <div> <span>{count}</span> <button onClick={add}> + </button> </div> ); }
点击按钮后执行结果为
count的值为 1 setInterval中 count 的值为: 0
原因解析
依赖为 [] 的 useEffect 仅在组件初次渲染时执行一次,所以 setInterval中 count 的值为 0
点击 + 按钮后,count 变为1,并触发组件重新渲染,此时会生成新的state对象快照,但 setInterval 内一直取的组件初次渲染时创建的state对象的快照,所以一直是0,不会变化!
解决方案
- 依赖项改为 count
- 每次组件重新渲染时,都清除计时器(否则每次 count 变化,都会生成一个新的计时器)
import { useState, useEffect } from "react"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount(count + 1); }; useEffect(() => { const timer = setInterval(() => { console.log(`setInterval中 count 的值为: ${count}`); }, 1000); return () => clearInterval(timer); }, [count]); console.log("count的值为", count); return ( <div> <span>{count}</span> <button onClick={add}> + </button> </div> ); }
useCallback 闭包陷阱
Father.jsx
import { useState } from "react"; import Child1 from "./Child1.jsx"; export default function Father() { const [count, setCount] = useState(0); const add = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={add}> + </button> <Child1 count={count} /> </div> ); }
Child1.jsx
import { useCallback } from "react"; function Child1({ count }) { let log = useCallback(() => { console.log("useCallback中count的值为: ", count); }, []); return ( <div style={{ border: "1px solid", padding: "10px", margin: "10px" }}> <h1>子组件1</h1> <p>count: {count}</p> <button onClick={() => log()}> 打印 </button> </div> ); } export default Child1;
先点击父组件 + 号,再点击子组件里的 打印 按钮,结果为
原因解析
useCallback 依赖为 [] 时,取到是子组件初次渲染时获取到的来自父组件的 count 值 0 ,当父组件的 count 变化后,useCallback 不会重新取值,所以 count 不会变化
解决方案
将 useCallback 的依赖为 count
let log = useCallback(() => { console.log("useCallback中count的值为: ", count); }, [count]);
自测题
第1题
点击 更新 按钮后,页面 count 和控制台打印的值
import { useState } from "react"; export default function Father() { const [count, setcount] = useState(10); function updateCount() { setcount(count + 1); console.log("1-", count); } return ( <div> <p>{count}</p> <button onClick={updateCount}>更新</button> </div> ); }
答案
页面的 count 为 11 控制台打印 1- 10
第2题
点击 更新 按钮后,页面 count 和控制台打印的值
import { useState } from "react"; export default function Father() { const [count, setcount] = useState(10); function updateCount() { setcount(count + 1); console.log("1-", count); setcount(count + 1); console.log("2-", count); } return ( <div> <p>{count}</p> <button onClick={updateCount}>更新</button> </div> ); }
页面的 count 为 11 控制台打印 1- 10 2- 10
第3题
点击 更新 按钮后,页面 count 和控制台打印的值
import { useState } from "react"; export default function Father() { const [count, setcount] = useState(10); function updateCount() { setcount(count + 1); console.log("1-", count); setcount((count) => count + 1); console.log("2-", count); } return ( <div> <p>{count}</p> <button onClick={updateCount}>更新</button> </div> ); }
答案
页面的 count 为 12 控制台打印 1- 10 2- 10
第4题
点击 更新 按钮后,页面 count 和控制台打印的值
import { useState } from "react"; export default function Father() { const [count, setcount] = useState(10); function updateCount() { setTimeout(() => { setcount(count + 1); setcount(count + 1); console.log("1-", count); setcount(count + 1); setcount(count + 1); console.log("2-", count); }); } return ( <div> <p>{count}</p> <button onClick={updateCount}>更新</button> </div> ); }
答案
页面的 count 为 11 控制台打印 1- 10 2- 10