为什么 addEvenetListener 上的回调打印出旧状态?

pin*_*o64 4 javascript reactjs

我有一个回调函数,我在其中设置了一些状态。国家似乎变异得很好。但是,我无法引用回调函数内的更新状态 - 它总是打印出初始状态。

所以我的问题是为什么会发生这种情况,如果我想检查回调内的更新状态,我应该如何进行?

import React, { useState, useEffect } from "react";
import Context from "./ctx";

export default props => {
  const [state, setState] = useState({
    x: 0,
    y: 0
  });

  useEffect(() => {
    window.addEventListener("resize", e => {
      setState({
        x: e.target.window.visualViewport.width,
        y: e.target.window.visualViewport.height
      });
      console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
    });

    return () => {
      window.removeEventListener("resize");
    };
  }, []);

  console.log(`${JSON.stringify(state)}`); <<---logs updated versions of the state.

  return (
    <Context.Provider
      value={{
        ...state
      }}
    >
      {props.children}
    </Context.Provider>
  );
};
Run Code Online (Sandbox Code Playgroud)

小智 5

您面临的问题与两件事有关:

  1. React 是如何工作的
  2. 闭包(函数+创建它的环境)。

1. React 如何工作

您已经创建并导出了函数式 React 组件,该组件接受一些 props,使用钩子进行状态管理,并呈现一些内容。每当组件(或其父组件)中的某些 props 或状态发生变化时,react 都会重新渲染您的组件,这意味着:它实际上会调用您的函数,例如yourComponent(props). 要点是:每次重新渲染发生时,函数体都会被执行,同时还会调用useStateuseEffect

2. 闭包(函数+创建它的环境)

每当我们在 JavaScript 中创建/定义某个函数时,它都会与创建它的环境一起存储在运行时内存中。在您的情况下,您在此处定义相关函数(并useEffect同时将其作为回调提供):

() => {
    window.addEventListener("resize", e => {
      setState({
        x: e.target.window.visualViewport.width,
        y: e.target.window.visualViewport.height
      });
      console.log(`${JSON.stringify(state)}`); <<---logs the initial state.
    });

    return () => {
      window.removeEventListener("resize");
    };
  }
Run Code Online (Sandbox Code Playgroud)

它创建的环境是功能组件的主体。
因此,每当 React 调用你的组件时,就会创建新的环境,并定义新的回调函数。但是,因为您在此处提供了空数组作为第二个参数(表示依赖项数组 docs):

useEffect(() => {
    window.addEventListener("resize", e => {
      setState({
        x: e.target.window.visualViewport.width,
        y: e.target.window.visualViewport.height
      });
      console.log(`${JSON.stringify(state)}`);
    });

    return () => {
      window.removeEventListener("resize");
    };
  }, []); // <-- HERE 
Run Code Online (Sandbox Code Playgroud)

提供的函数将仅执行一次 - 在组件安装/首次渲染时,因为您说它不依赖于任何其他值,所以不需要再次触发该效果。
底线:您在每个渲染上定义新的回调函数,但实际上只调用了第一个回调函数。当它被定义时,状态的值为{ x: 0, y: 0 }。这就是为什么你总是能获得这个价值。

解决方案

根据您的需要,您可以在第二个参数中设置依赖项,例如:

useEffect(() => {
    window.addEventListener("resize", e => {
      setState({
        x: e.target.window.visualViewport.width,
        y: e.target.window.visualViewport.height
      });
      console.log(`${JSON.stringify(state)}`); 
    });

    return () => {
      window.removeEventListener("resize");
    };
  }, [state]); // <-- NOW THE CALLBACK WILL BE EXECUTED EVERY TIME STATE CHANGES
Run Code Online (Sandbox Code Playgroud)

或者,您可以完全省略第二个参数,这样它将在每次渲染时触发:

useEffect(() => {
    window.addEventListener("resize", e => {
      setState({
        x: e.target.window.visualViewport.width,
        y: e.target.window.visualViewport.height
      });
      console.log(`${JSON.stringify(state)}`);
    });

    return () => {
      window.removeEventListener("resize");
    };
  }); // <-- WITHOUT SECOND ARGUMENT
Run Code Online (Sandbox Code Playgroud)

它可能看起来很昂贵,但官方 React 文档说它不应该对事件侦听器的使用产生任何性能影响,因为 addEventListener DOM api 非常高效。但如果您发现一些性能问题,您始终可以使用第一个解决方案:)。

希望能帮助到你!