很多React开发者都遇到过useEffect中使用事件监听在回调函数中获取到旧的state值的问题,也都知道如何去解决。这个问题网上很多讲解都是直接讲是因为闭包导致获取到的是旧的state值,讲的不够清晰。我们看下具体的例子来逐步理解这个问题。
首先看一个手动实现的简易useEffect的事件监听的例子
代码语言:javascript复制import React, { useRef, useState } from 'react'; // "react": "^18.1.0",
import ReactDOM from 'react-dom/client';
let memoizedState: any[] = [];
let currentIndex = 0;
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
const App:React.FC = () => {
const [hasAddEventListener, setHasAddEventListener] = useState(false);
const[count, setCount] = useState(1);
const btn = useRef<any>(null);
const onAddEventListenerShowCount = () => {
console.log('onAddEventListenerShowCount count:', count);
}
useEffect(() => {
console.log('btn.current:', btn.current);
btn.current?.addEventListener?.('click', onAddEventListenerShowCount)
return () => {
btn.current?.removeEventListener?.('click', onAddEventListenerShowCount)
}
},[hasAddEventListener]);
const onAddEventListener = () => setHasAddEventListener(true);
const onAddClick = () => {
const newCount = count 1;
setCount(newCount);
console.log('onAddClick count:', count);
console.log('onAddClick newCount:', newCount);
}
const showCount = () => {
console.log('showCount count:', count);
}
return (
<div className="App">
<div>top</div>
<button onClick={onAddEventListener}>addEventListener</button>
<button ref={btn}>addEventListenerShowCount</button>
<button onClick={onAddClick}>add</button>
<button onClick={showCount}>showCount</button>
</div>
);
}
// 自定义的useEffect
function useEffect(fn: any, watch: any[]) {
if (currentIndex === 0) {
fn();
memoizedState[currentIndex] = watch;
currentIndex ; // 累加 currentIndex
}
const hasWatchChange = memoizedState[currentIndex - 1]
? !watch.every((val, i) => val === memoizedState[currentIndex - 1][i])
: true;
console.log('hasWatchChange:', hasWatchChange)
if (hasWatchChange) {
fn();
memoizedState[currentIndex] = watch;
currentIndex ; // 累加 currentIndex
}
}
function render() {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
currentIndex = 0; // 注意将 effectCursor 重置为0
}
render();
渲染的页面如下
依次点击 addEventListener // 点击addEventListener按钮 添加eventListener监听事件
addEventListenerShowCount // 点击addEventListenerShowCount的按钮 eventListener事件回调函数打印state值
add // 点击add按钮 设置新的state值
showCount // 点击showCount按钮 打印state值
addEventListenerShowCount // 再次点击addEventListenerShowCount的按钮 eventListener事件回调函数打印state值
控制台打印结果如下
手动实现的简易useEffect中,事件监听回调函数中也会有获取不到state最新值的问题
下面根据上面React代码模拟为常规的js代码
代码语言:javascript复制let obj; // 模拟btn元素
const App = (addOne) => { // 模拟React App纯函数组件
let a = 1; // 模拟state
obj = obj || {
showA: () => { // 模拟eventListener的回调函数
console.log('obj a:', a);
},
}
if (addOne) { // 模拟修改state值
a = 1;
}
console.log('App a:', a);
}
全局作用域的obj对象类似于按钮btn ref
App函数类似React App纯函数组件
每次state变化,React 函数会重新执行,所以我们可以进行如下模拟操作
这个示例的运行过程就比较好理解,第一次执行App函数,初始化数据,Obj可以获取到函数内的a变量,因此,变量a所分配的内存不会释放,再运行App函数,Obj获取到的变量a始终是第一次初始化时的a在内存中指向的值。在React函数中也是一样的情况,某一个对象的监听事件的回调函数,这个对象相当于全局作用域变量(或者与函数同一层作用域链),在回调函数中获取到的state值,为第一次运行时的内存中的state值。而组件函数内的普通函数,每次运行组件函数中,普通函数与state的作用域链为同一层,所以会拿到最新的state值。