在review同事的代码的时候,发现了一个问题,从而延伸出对react闭包缺陷的思考,在日常的开发中我们还是要尽量避免这个问题的发生。
react闭包陷阱原理
react闭包的陷阱是函数子组件 Hooks 中场景的一种现象。表现为在异步回调或者副作用中捕获到了旧的state或者props值,而不是最新值,究其根本原因是源于 JS 闭包的特性和 React 函数组件渲染机制的结合产生的一种陷阱,没有合理的设置 Hooks 的依赖数组。
js 闭包特性
函数会“记住”并访问它被创建时的词法作用域,即便是后续在其它地方被调用。
React 函数组件渲染机制
组件每次渲染都会经历:
- 创建新的函数作用域
- 重新执行组件函数体
- 生成全新的 state、props和函数
react闭包陷阱的场景
在使用 useEffect hook 的时候,涉及到 回调函数或者使用state的时候,没有合理设置依赖数组,就会出现问题。
场景1:定时器
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
console.log('interval', count)
setCount(count + 1)
}, 1000)
return () => {
clearInterval(interval)
}
}, []) // 依赖数组为空
console.log('count', count)
return (
<div>
<p>当前Count: {count}</p>
</div>
)
}
可以看到以下的执行结果,在 setInterval中拿到的 count 始终是初始化的值。
场景2:回调函数事件监听
function App() {
const [count, setCount] = useState(0)
const handleDocument = (event) => {
console.log('document clicked', count)
}
useEffect(() => {
document.addEventListener('click', handleDocument, false)
return () => {
document.removeEventListener('click', handleDocument)
}
}, [])
console.log('render count', count)
const handleClick = (event) => {
event.stopPropagation() // 阻止事件冒泡
setCount(count + 1)
}
return (
<div>
<p>当前Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}
执行结果如下:多次点击按钮更新count的值,后续点击document的事件,始终获取的是count初始值
如何解决
使用函数式更新
useEffect(() => {
const interval = setInterval(() => {
console.log('interval', count)
setCount((prev) => prev + 1) // 使用函数式更新
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
合理设置依赖项
定时器设置依赖
useEffect(() => {
const interval = setInterval(() => {
console.log('interval', count) // 每次获取的是上一次的值
setCount(count + 1) // 设置新值,触发更新
}, 1000)
return () => {
clearInterval(interval)
}
}, [count]) // 依赖count,每次count变化时,重新执行effect
执行结果如下
回调函数使用 useCallback 设置依赖
function App() {
const [count, setCount] = useState(0)
// 使用 useCallback 定义回调函数
const handleDocument = useCallback(() => {
console.log('document clicked', count)
}, [count]) // 设置争取的依赖
useEffect(() => {
document.addEventListener('click', handleDocument)
return () => {
document.removeEventListener('click', handleDocument)
}
}, [handleDocument]) // 设置依赖
const handleClick = (event) => {
event.stopPropagation() // 阻止事件冒泡
setCount(count + 1)
}
return (
<div>
<p>当前Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}
执行结果如下
使用useRef
function App() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
countRef.current = count // 使用ref 保存count的值
useEffect(() => {
const interval = setInterval(() => {
console.log('interval', countRef.current) // 获取ref 的值
}, 1000)
return () => {
clearInterval(interval)
}
}, [])
console.log('render count', count)
const handleClick = (event) => {
event.stopPropagation()
setCount(count + 1)
}
return (
<div>
<p>当前Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}
执行结果如下
实践总结
- 在 useEffect 中使用了 state 和 props 都要声明依赖数组
- 在 useEffect 中更新 state 的时候,可以通过函数式更新
- 在 useEffect 中使用回调函数,要配合
useCallback,然后声明依赖数组