【问题标题】:Callback function for ClickAwayListener stop midway on executionClickAwayListener 的回调函数在执行中途停止
【发布时间】:2021-03-10 06:51:10
【问题描述】:

我正在为我的应用程序使用 Material-UI ClickAwayListenerreact-router。我遇到的错误是,当ClickAwayListener 的回调被执行时,它会在useEffect 运行中途停止,然后它会恢复运行。回调不应出现此行为。回调应该在useEffect 可以运行之前完全执行。下面是我为演示问题而创建的代码和代码的this is the demo

import React, { useEffect, useState } from "react";
import ClickAwayListener from "@material-ui/core/ClickAwayListener";

import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link,
  useHistory,
  useParams
} from "react-router-dom";

export default function BasicExample() {
  return (
    <Router>
      <div>
        <Switch>
          <Route exact path="/">
            <ButtonPage />
          </Route>
          {/*Main focus route here*/ }
          <Route path="/:routeId">
            <Home />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// Main focus here
function Home() {
  const history = useHistory();
  const { routeId } = useParams();

  const [count, setCount] = useState(0);

  const handleClick1 = () => {
    history.push("/route1");
  };
  const handleClick2 = () => {
    history.push("/route2");
  };

  // useEffect run on re-render and re-mount
  useEffect(() => {
    console.log("Re-render or remount");
  });
  
  useEffect(() => {
    console.log("Run on route change from inside useEffect");
  }, [routeId]);

  const handleClickAway = () => {
    console.log("First line in handle click away");
    setCount(count + 1);
    console.log("Second line in handle click away");
  };

  return (
    <div>
      <button onClick={handleClick1}>Route 1 </button>
      <button onClick={handleClick2}>Route 2 </button>
      <ClickAwayListener onClickAway={handleClickAway}>
        <div style={{ height: 100, width: 100, backgroundColor: "green" }}>
          Hello here
        </div>
      </ClickAwayListener>
    </div>
  );
}

// Just a component such that home route can navigate
// Not important for question
function ButtonPage() {
  const history = useHistory();

  const handleClick1 = () => {
    history.push("/route1");
  };
  const handleClick2 = () => {
    history.push("/route2");
  };

  return (
    <div>
      <button onClick={handleClick1}>Route 1 </button>
      <button onClick={handleClick2}>Route 2 </button>
    </div>
  );
}

具体来说,当我在ClickAwayListener 之外点击时,handleClickAway 正常运行并且日志消息是

First line in handle click away
Second line in handle click away
Re-render or remount 

直到我选择点击导航到其他路线的button。事情变得很奇怪:handleClickAway 运行第一行日志记录,然后useEffect 运行并打印它的日志记录,然后handleClickAway 恢复并打印它的第二行。所以如果我这样做,这就是日志记录

First line in handle click away
Re-render or remount
Run on route change from inside useEffect
Second line in handle click away

在对此错误进行一些测试后,我发现导致此错误的原因是handleClickAway 中的setCount。如果我删除此行,函数 handleClickAway 将在所有情况下按预期运行。我的结论是,更改组件状态,或者我应该说,执行任何导致组件在 handleClickAway 内重新渲染的操作与路由导航相结合都会导致此错误。

这种行为很奇怪,因为据我所知,正常的、与 Promise 无关的回调无法像这样中途停止。我猜ClickAwayListener 不知何故将handleClickAway 变成了 Promise 之类的东西。但即便如此,它也没有理由停在setCount 并让useEffect 运行? 谁能给我解释一下?

编辑 1:正如@Rex Pan 在此答案中指出的那样,handleClickAway 似乎在其中运行 useEffect。这是我通过阅读跟踪堆栈可以得出的唯一结论。但是这种行为只有在用户点击导航到不同路线的链接时才会发生。单击旁边的其他区域不会导致此错误。有人可以解释一下为什么以及如何发生这种情况吗?

编辑 2:阅读@Rex Pan 的回答后,我做了更多调查。在我看来,他的回答是正确的,但只适用于ClickAwayListener 回调。所以下面的代码会产生和上面一样的“bug”

export default function ClickAway() {
  const classes = useStyles();
  const [count, setCount] = React.useState(0);

  useEffect(() => {
    console.log("Test in useEffect");
  });

  const handleClickAway = () => {
    console.log("Count is " + count);
    setCount(count + 1);
    setCount(count + 10);
    console.log("Count is " + count);
  };

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <div className={classes.root}>Click away</div>
    </ClickAwayListener>
  );
}

但是,他的解释在其他情况下不起作用,例如下面的代码

export default function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("Run inside useEffect");
  });

  const handleClick = () => {
    console.log("First line");
    setCount(count + 1);
    console.log("Second line");
    setCount(count + 100);
    console.log("Third line");
  };

  return (
    <>
      <div>Count is: {count} </div>
      <button onClick={handleClick}>Click</button>
    </>
  );
}

我正在尝试阅读ClickAwayListner 的源代码,但到目前为止还没有找到导致此行为的位置。有人能指出这一点吗?

【问题讨论】:

    标签: javascript reactjs callback react-router material-ui


    【解决方案1】:

    setCount() 会导致渲染,该渲染将刷新之前渲染中的 useEffect()。这导致useEffect()s 在setCount() 返回handleClickAway() 之前被调用。

    handleClickAway()
        console.log("First line in handle click away");
        setCount(count + 1);
            ...
                effect0000000()
                    console.log("Re-render or remount");
                effect_on_routeId()
                    console.log("Run on route change");
        console.log("Second line in handle click away");
    

    您可以通过添加函数名称和new Error() 来验证并检查调用堆栈。

    useEffect(function effect0000000() {
        console.log(new Error())
        console.log("Re-render or remount");
      });
    
      useEffect(function effect_on_routeId() {
        console.log(new Error())
        console.log("Run on route change");
      }, [routeId]);
    
      const handleClickAway = function handleClickAway() {
        console.log("First line in handle click away");
        setCount(count + 1);
        console.log("Second line in handle click away");
      };
    

    如果你点击外面,

    1. handleClickAway()被调用,记录First line in handle click away
    2. setCount() 被调用,React 将重新渲染2,这将安排useEffect() 绑定到2
    3. Home()返回,setCount()返回
    4. handleClickAway() 登录 Second line in handle click away
    5. useEffect() 绑定到2 按计划调用

    当路由按钮被点击时

    1. history.push() 导致渲染 2,这将安排 useEffect() 绑定到 this = 2
    2. handleClickAway() 调用并记录 First line in handle click away
    3. setCount() 被调用
    4. React 需要在下一次渲染之前刷新(调用)之前的效果(绑定到 this = 2
    5. React 调用 Home() 用于渲染 3,这将安排 useEffect() 绑定到 3
    6. Home()返回,setCount()返回
    7. handleClickAway()日志Second line in handle click away
    8. useEffect() 绑定到3 按计划调用

    【讨论】:

    • 但如果是这样,那为什么要先打印handleClickAway 的登录信息?更奇怪的是,当它的执行中途停止以打印 useEffect 的日志记录,然后继续(?)运行?
    • 另外我不认为错误堆栈跟踪意味着useEffecthandleClickAway 之前运行。正如this article 指出的那样,该消息可能意味着handleClickAway 在其内部以某种方式调用useEffect
    • 还值得指出的是,如果您删除handleClickAway 中的setCount,您将看到handleClickAway 不再在堆栈跟踪中。我得到的唯一结论是,通过调用setCounthandleClickAway 以某种方式在其内部运行useEffect
    • 是的,我从一开始就是这么说的。 setState 在返回到 handleClickAway() 之前调用 useEffect()s。
    • 我认为情况并非如此。尝试单击其他部分,但按钮您会看到日志的顺序不同。如果你说的是对的,那么顺序应该是一样的。
    【解决方案2】:

    这就是发生这种情况的原因。 useEffect 使用 routeId 作为其道具之一。当 handleClickAway 触发时,您可能正在导航,所以 useEffect 被触发,然后记录到您的控制台。

    另外,您确定它正在停止执行,而不仅仅是异步登录吗?通常 useEffects 不会阻止任何其他代码的执行。

    一个简单的测试方法是延迟 useEffect 中的日志记录。如果它没有阻止执行,则应首先显示您的其他日志,然后再显示此日志。

    useEffect(() => {
       setTimeout(() => console.log('delayed log'), 3000)
    }, [routeId])
    

    回答您的评论:

    根据this 文章,任何异步调用都是在调用堆栈之外完成的,因此它实际上不会阻塞代码的执行。

    简而言之,Javascript中的异步实现就完成了 通过调用堆栈、回调队列、Web API 和事件循环。

    React 很可能对 useEffect 做同样的事情,所以这并不意味着它停止执行你的回调来运行 useEffect,只是它在它旁边运行它。

    在文章本身中,他使用了一个超时示例,就像我在这里所做的那样,然后解释了它是如何在堆栈中处理的:

    在 Javascript 中,所有指令都放在调用堆栈上。当。。。的时候 堆栈到达 setTimeout,引擎将其视为 Web API 指令并将其弹出并发送到 Web API。一旦 Web API 执行完毕,将到达回调队列。

    【讨论】:

    • 我编辑了我的代码,如您所见,如果我不点击按钮,另一个useEffect 仍然可以正常工作。同样在您的观点中,为什么useEffect 在回调已经运行时记录消息? JS 主堆栈假设一次只运行一件事。
    • 我在帖子中回复了你的评论 :)
    • handleClickAwayuseEffect 都是某种形式的回调运行,因此它确实不会阻塞代码执行。但是这些回调不会在调用堆栈上运行——它们只是在那里等待,只有在主堆栈中没有任何内容时才会被推送到主堆栈并执行。一旦它们在主堆栈中并执行,该回调就没有理由停止并让其他回调运行,因为该回调现在已经阻塞了主堆栈。您提到的文章似乎并未涵盖所有方面。观看this JSConf video
    【解决方案3】:

    当您单击路由按钮时,会同时触发两个事件,并且两个处理程序内部的 history.push() 和 setcount() 之间存在竞争条件,因此发生了两个独立的重新渲染,每个重新渲染都会触发一个效果.所以这不是一个错误,两个处理函数同时执行它们的工作,每个函数都执行其中的所有代码。

    【讨论】:

    • 我不认为这是正确的。 JavaScript 是一种单线程语言,所以竞争条件真的不是问题。当一个函数在主堆栈中运行时,没有其他函数可以与之“竞争”。
    • 你是对的,但是当涉及到诸如反应状态和效果之类的异步任务时,执行堆栈的顺序可能并不像预期的那样。 @rex-pan 在跟踪执行顺序方面做得很好。看起来 handleClickAway 函数中的所有代码都没有阻止其他函数的执行。在这种情况下,通常使用术语“竞争条件”
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多