React useEffect中使用事件监听在回调函数中state不更新的问题

2022-07-31 14:14:52 浏览数 (1)

很多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值。

0 人点赞