javascript - React 在接收到多个并发事件后如何重新渲染是否有明确定义的行为?

标签 javascript reactjs react-hooks event-handling dom-events

假设我有一个 React 组件,它可能会接收多个事件,这些事件在一次交互后执行状态更改,如下所示:

const {useRef, useState} = React;

function App() {
  const [_, setState] = useState(null);

  const renderCount = useRef(0);
  renderCount.current += 1;
  console.log("rendering");
  
  return (
  <div>
    {`Render count: ${renderCount.current} `}
    
    <span onClick={() => { 
      console.log("span clicked"); 
      setState({}); 
    }}>
      <button onClick={() => { 
        console.log("button clicked"); 
        setState({}); 
      }}>Send click events</button>
    </span>
  </div>
  );
}

ReactDOM.createRoot(
  document.getElementById("root")
).render( < App / > );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

<div id="root"></div>

在上面的示例中,按下按钮触发单击事件后,两个事件处理程序都会运行,但 React 会将状态更新批量处理为仅重新渲染一次。

但是,如果我将这些事件处理程序直接附加到 native HTML 元素上(如下所示),React 会在每次交互时重新渲染两次,或者在每个事件处理程序之后重新渲染两次。

const {useRef, useState, useEffect} = React;

function App() {
  const [_, setState] = useState(null);
  const spanRef = useRef();
  const btnRef = useRef();

  const renderCount = useRef(0);
  renderCount.current += 1;
  console.log("rendering");
  
  useEffect(() => {
    const handler1 = () => { 
      console.log("span clicked"); 
      setState({}); 
    };
    
    const handler2 = () => { 
      console.log("button clicked"); 
      setState({}); 
    };
    
    const span = spanRef.current;
    span.addEventListener("click", handler1);
    
    const btn = btnRef.current;
    btn.addEventListener("click", handler2);

    return () => {
      span.removeEventListener("click", handler1);
      btn.removeEventListener("click", handler2);
    };
  }, []);
  
  return (
  <div>
    {`Render count: ${renderCount.current} `}
    
    <span ref={spanRef}>
      <button ref={btnRef}>Send click events</button>
    </span>
  </div>
  );
}

ReactDOM.createRoot(
  document.getElementById("root")
).render( < App / > );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

<div id="root"></div>

我怀疑这与 React 的合成事件系统有关,但我正在尝试准确地理解该行为。例如,React 是否总是在 native 事件监听器之后立即重新渲染状态更新?如果是这样,效果是否仍在重新渲染之间运行,或者它们是否推迟到所有事件处理程序执行完毕为止?

无论是答案还是引用官方文档,我们都会表示赞赏。需要澄清的是,这并不是针对我正在解决的特定项目或问题,我只是想更好地理解 React 的行为。

最佳答案

区别在于(很可能)

我希望 React 没有很好地记录这一点,因为你询问事情发生的顺序,而在 React 中你不应该关心这些。

我什至会说,React 可能随时改变内部行为,而不改 rebase 本概念(例如 React 对状态的保证)。

无论如何,我会在这里尝试理解它(抱歉,我需要猜测一下) ...

react 事件

可能相关的事实是React attaches event handlers at the root .

您可以通过记录 event.nativeEvent property.currentTarget 来确认这一点,例如:

<span onClick={ event => {
    console.log("span clicked:", event.nativeEvent.currentTarget, event.nativeEvent.target);
    setState({});
}}>
    <button onClick={ event => {
        console.log("button clicked:", event.nativeEvent.currentTarget, event.nativeEvent.target);
        setState({});
    }}>Send click events</button>
</span>

原生事件

在javascript中,没有任何东西可以并行运行,所以正常的点击事件, 由例如发起当其他 javascript 代码仍在运行时,无法执行鼠标操作。

因此,点击事件会排队,并且每个事件仅在 call stack 时执行。 (又名“执行堆栈”)为空。

因此,

  • 按钮点击处理程序与setState和虚拟DOM的重新渲染一起执行,
  • 然后可以执行 span click 处理程序,并执行第二个 setState 和重新渲染,
  • 然后实际的浏览器 DOM 就会更新。

合成事件

假设 React 使用相同的内部事件处理程序处理来自多个元素的事件,并且 使用 synthetic events 调用附加的处理程序.

由于合成事件始终同步执行,因此所有附加的事件处理程序都会在内部 React 事件处理程序返回之前执行,即

  • button onClicksetState 一起执行,但随后不是重新渲染,
  • 执行span click处理程序,并执行第二个setState
  • 然后会发生重新渲染和 DOM 更新。

检查事件

无法检查或“利用”已附加的事件处理程序 (除了“检查节点”和 getEventListeners 等浏览器开发工具),但您可以进一步检查事件执行的顺序,如果您

  • 将 native 事件处理程序和 React 事件处理程序添加到元素中,
  • microtask 中添加另一个 console.log ,这将说明调用堆栈是空的。

例如:

const handler1 = () => {
    console.log("Native event: spanRef click");
    queueMicrotask(() => { console.log('Native event: spanRef click (microtask)'); } )
    setState({});
};

const handler2 = () => {
    console.log("Native event: buttonRef click");
    queueMicrotask( () => { console.log('Native event: buttonRef click (microtask)'); } )
    setState({});
};

// ...

<span ref={ spanRef } onClick={ () => {
    console.log('React event: span onClick');
    queueMicrotask( () => {
        console.log('React event: span onClick (microtask)');
    })
    setState({});
}}>
    <button ref={ buttonRef } onClick={ () => {
        console.log( 'React event: button onClick' );
        queueMicrotask( () => {
            console.log( 'React event: button onClick (microtask)' );
        })
        setState({});
    }}>Send click events</button>
</span>

鼠标单击按钮后将记录:

rendering 1
Native event: buttonRef click               \
rendering 2                                  | native button event completely executed
Native event: buttonRef click (microtask)   /
Native event: spanRef click                 \
rendering 4                                  | native span event completely executed
Native event: spanRef click (microtask)     /
     (Here probably Reacts internal root event handler executed)
React event: button onClick                 \
React event: span onClick                    |
rendering 6                                  | All React events executed
React event: button onClick (microtask)      |
React event: span onClick (microtask)       /

关于javascript - React 在接收到多个并发事件后如何重新渲染是否有明确定义的行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75872279/

相关文章:

javascript - JavaScript 中带有数组模板的 functionFactory

reactjs - 在 React 中设置复选框 "check"属性

reactjs - react 路由器,history.push 之后的代码运行?

reactjs - 不使用 useEffect Hook 获取 API 是否错误?

reactjs - 在 React Native 中使用 useContext 的上下文 API 显示 TypeError

javascript - 动态表单为第一个和第二个输入返回相同的值

javascript - React - useState 钩子(Hook)访问状态

JavaScript - 使用数据创建数组而不循环

javascript - 添加到数组兄弟

javascript - 使用 Gulp 将 jsx 转换为 js 时出现 React 语法错误